summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/inputmethodservice/InputMethodService.java25
-rw-r--r--core/java/android/os/Binder.java25
-rw-r--r--core/java/android/provider/CallLog.java12
-rw-r--r--core/java/android/view/ViewRootImpl.java10
-rw-r--r--core/java/android/view/WindowManager.java15
-rw-r--r--core/java/com/android/internal/app/ResolverActivity.java7
-rw-r--r--core/java/com/android/internal/statusbar/IStatusBar.aidl13
-rw-r--r--core/res/res/values/config.xml6
-rw-r--r--core/res/res/values/symbols.xml2
-rw-r--r--core/tests/coretests/src/android/os/AidlTest.java33
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java5
-rw-r--r--media/java/android/media/audiofx/Visualizer.java10
-rw-r--r--packages/SystemUI/Android.bp17
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt95
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt111
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/pager/Pager.kt357
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/pager/PagerState.kt348
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/pager/SnappingFlingBehavior.kt270
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt167
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt437
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt16
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt3
-rw-r--r--packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_down.xml31
-rw-r--r--packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_up.xml31
-rw-r--r--packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml47
-rw-r--r--packages/SystemUI/res/drawable/ic_person_outline.xml26
-rw-r--r--packages/SystemUI/res/drawable/immersive_cling_bg_circ.xml2
-rw-r--r--packages/SystemUI/res/drawable/immersive_cling_light_bg_circ.xml2
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_background_circle.xml29
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_background_large_top_large_bottom.xml39
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_background_large_top_small_bottom.xml39
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_background_small_top_large_bottom.xml39
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_background_small_top_small_bottom.xml39
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_check_icon.xml25
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_default_permission_icon.xml25
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_down.xml30
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_up.xml30
-rw-r--r--packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml2
-rw-r--r--packages/SystemUI/res/layout/immersive_mode_cling.xml6
-rw-r--r--packages/SystemUI/res/layout/privacy_dialog_card_button.xml26
-rw-r--r--packages/SystemUI/res/layout/privacy_dialog_item_v2.xml89
-rw-r--r--packages/SystemUI/res/layout/privacy_dialog_v2.xml109
-rw-r--r--packages/SystemUI/res/values/colors.xml3
-rw-r--r--packages/SystemUI/res/values/dimens.xml3
-rw-r--r--packages/SystemUI/res/values/ids.xml4
-rw-r--r--packages/SystemUI/res/values/strings.xml47
-rw-r--r--packages/SystemUI/res/values/styles.xml18
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java18
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt81
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt48
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModel.kt177
-rw-r--r--packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/flags/Flags.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt54
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt32
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt664
-rw-r--r--packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt65
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt367
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogEvent.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt539
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java39
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/shared/model/RemoteUserInput.kt35
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java27
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java45
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java590
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java23
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java29
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSettingsController.java167
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java31
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt91
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt38
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt277
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt62
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt13
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt69
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt49
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt825
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogV2Test.kt322
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt55
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt12
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt11
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt36
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt85
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java15
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt87
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt90
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSettingsControllerTest.kt245
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java81
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt31
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt5
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt11
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java15
-rw-r--r--services/core/java/com/android/server/display/DisplayManagerService.java5
-rw-r--r--services/core/java/com/android/server/display/DisplayPowerController.java6
-rw-r--r--services/core/java/com/android/server/display/DisplayPowerController2.java4
-rw-r--r--services/core/java/com/android/server/display/HighBrightnessModeController.java43
-rw-r--r--services/core/java/com/android/server/display/HighBrightnessModeMetadataMapper.java3
-rw-r--r--services/core/java/com/android/server/display/TEST_MAPPING21
-rw-r--r--services/core/java/com/android/server/input/InputManagerService.java12
-rw-r--r--services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java11
-rw-r--r--services/core/java/com/android/server/inputmethod/InputMethodManagerService.java8
-rw-r--r--services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java37
-rw-r--r--services/core/java/com/android/server/media/projection/mediaprojection.md5
-rw-r--r--services/core/java/com/android/server/notification/NotificationManagerService.java49
-rw-r--r--services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java14
-rw-r--r--services/core/java/com/android/server/statusbar/StatusBarManagerService.java26
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperManagerService.java6
-rw-r--r--services/core/java/com/android/server/wm/ActivityStarter.java33
-rw-r--r--services/core/java/com/android/server/wm/ActivityTaskSupervisor.java4
-rw-r--r--services/core/java/com/android/server/wm/Dimmer.java15
-rw-r--r--services/core/java/com/android/server/wm/DisplayContent.java2
-rw-r--r--services/core/java/com/android/server/wm/DisplayPolicy.java88
-rw-r--r--services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java7
-rw-r--r--services/core/java/com/android/server/wm/LetterboxConfiguration.java29
-rw-r--r--services/core/java/com/android/server/wm/TransitionController.java11
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerShellCommand.java12
-rw-r--r--services/core/java/com/android/server/wm/WindowState.java8
-rw-r--r--services/core/jni/com_android_server_input_InputManagerService.cpp13
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java11
-rw-r--r--services/tests/displayservicetests/TEST_MAPPING10
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java6
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java7
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java60
-rwxr-xr-xservices/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java245
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java12
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DimmerTests.java37
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TransitionTests.java21
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java1
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java7
-rw-r--r--services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java38
-rw-r--r--services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java30
-rw-r--r--services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java7
-rw-r--r--telephony/java/android/telephony/SubscriptionManager.java9
-rw-r--r--tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt13
-rw-r--r--tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt152
-rw-r--r--tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt8
-rw-r--r--tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt10
-rw-r--r--tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt86
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml8
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml8
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java4
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java32
-rw-r--r--tests/Input/src/com/android/test/input/MotionPredictorTest.kt2
176 files changed, 7776 insertions, 2117 deletions
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 60b11b425184..7b5dd55f385b 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -807,9 +807,11 @@ public class InputMethodService extends AbstractInputMethodService {
onUnbindInput();
mInputBinding = null;
mInputConnection = null;
- // free-up cached InkWindow surface on detaching from current client.
+
if (mInkWindow != null) {
- removeHandwritingInkWindow();
+ finishStylusHandwriting();
+ // free-up InkWindow surface after timeout.
+ scheduleStylusWindowIdleTimeout();
}
}
@@ -1020,6 +1022,7 @@ public class InputMethodService extends AbstractInputMethodService {
mOnPreparedStylusHwCalled = true;
}
if (onStartStylusHandwriting()) {
+ cancelStylusWindowIdleTimeout();
mPrivOps.onStylusHandwritingReady(requestId, Process.myPid());
} else {
Log.i(TAG, "IME is not ready. Can't start Stylus Handwriting");
@@ -1109,7 +1112,7 @@ public class InputMethodService extends AbstractInputMethodService {
*/
@Override
public void removeStylusHandwritingWindow() {
- InputMethodService.this.removeStylusHandwritingWindow();
+ InputMethodService.this.finishAndRemoveStylusHandwritingWindow();
}
/**
@@ -2667,21 +2670,15 @@ public class InputMethodService extends AbstractInputMethodService {
* Typically, this is called when {@link InkWindow} should no longer be holding a surface in
* memory.
*/
- private void removeStylusHandwritingWindow() {
+ private void finishAndRemoveStylusHandwritingWindow() {
+ cancelStylusWindowIdleTimeout();
+ mOnPreparedStylusHwCalled = false;
+ mStylusWindowIdleTimeoutRunnable = null;
if (mInkWindow != null) {
if (mHandwritingRequestId.isPresent()) {
// if handwriting session is still ongoing. This shouldn't happen.
finishStylusHandwriting();
}
- removeHandwritingInkWindow();
- }
- }
-
- private void removeHandwritingInkWindow() {
- cancelStylusWindowIdleTimeout();
- mOnPreparedStylusHwCalled = false;
- mStylusWindowIdleTimeoutRunnable = null;
- if (mInkWindow != null) {
mInkWindow.hide(true /* remove */);
mInkWindow.destroy();
mInkWindow = null;
@@ -2707,7 +2704,7 @@ public class InputMethodService extends AbstractInputMethodService {
private Runnable getStylusWindowIdleTimeoutRunnable() {
if (mStylusWindowIdleTimeoutRunnable == null) {
mStylusWindowIdleTimeoutRunnable = () -> {
- removeHandwritingInkWindow();
+ finishAndRemoveStylusHandwritingWindow();
mStylusWindowIdleTimeoutRunnable = null;
};
}
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index 00676f3cb746..01e8fea1019d 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -926,16 +926,19 @@ public class Binder implements IBinder {
* @hide
*/
@VisibleForTesting
- public final @NonNull String getTransactionTraceName(int transactionCode) {
+ public final @Nullable String getTransactionTraceName(int transactionCode) {
+ final boolean isInterfaceUserDefined = getMaxTransactionId() == 0;
if (mTransactionTraceNames == null) {
- final int highestId = Math.min(getMaxTransactionId(), TRANSACTION_TRACE_NAME_ID_LIMIT);
+ final int highestId = isInterfaceUserDefined ? TRANSACTION_TRACE_NAME_ID_LIMIT
+ : Math.min(getMaxTransactionId(), TRANSACTION_TRACE_NAME_ID_LIMIT);
mSimpleDescriptor = getSimpleDescriptor();
mTransactionTraceNames = new AtomicReferenceArray(highestId + 1);
}
- final int index = transactionCode - FIRST_CALL_TRANSACTION;
- if (index < 0 || index >= mTransactionTraceNames.length()) {
- return mSimpleDescriptor + "#" + transactionCode;
+ final int index = isInterfaceUserDefined
+ ? transactionCode : transactionCode - FIRST_CALL_TRANSACTION;
+ if (index >= mTransactionTraceNames.length() || index < 0) {
+ return null;
}
String transactionTraceName = mTransactionTraceNames.getAcquire(index);
@@ -1300,19 +1303,9 @@ public class Binder implements IBinder {
final boolean hasFullyQualifiedName = getMaxTransactionId() > 0;
final String transactionTraceName;
- if (tagEnabled && hasFullyQualifiedName) {
+ if (tagEnabled) {
// If tracing enabled and we have a fully qualified name, fetch the name
transactionTraceName = getTransactionTraceName(code);
- } else if (tagEnabled && isStackTrackingEnabled()) {
- // If tracing is enabled and we *don't* have a fully qualified name, fetch the
- // 'best effort' name only for stack tracking. This works around noticeable perf impact
- // on low latency binder calls (<100us). The tracing call itself is between (1-10us) and
- // the perf impact can be quite noticeable while benchmarking such binder calls.
- // The primary culprits are ContentProviders and Cursors which convenienty don't
- // autogenerate their AIDL and hence will not have a fully qualified name.
- //
- // TODO(b/253426478): Relax this constraint after a more robust fix
- transactionTraceName = getTransactionTraceName(code);
} else {
transactionTraceName = null;
}
diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java
index ac6b2b23cdf5..3c5757dd5615 100644
--- a/core/java/android/provider/CallLog.java
+++ b/core/java/android/provider/CallLog.java
@@ -1985,13 +1985,14 @@ public class CallLog {
Log.w(LOG_TAG, "Failed to insert into call log; null result uri.");
}
+ int numDeleted;
if (values.containsKey(PHONE_ACCOUNT_ID)
&& !TextUtils.isEmpty(values.getAsString(PHONE_ACCOUNT_ID))
&& values.containsKey(PHONE_ACCOUNT_COMPONENT_NAME)
&& !TextUtils.isEmpty(values.getAsString(PHONE_ACCOUNT_COMPONENT_NAME))) {
// Only purge entries for the same phone account.
- resolver.delete(uri, "_id IN " +
- "(SELECT _id FROM calls"
+ numDeleted = resolver.delete(uri, "_id IN "
+ + "(SELECT _id FROM calls"
+ " WHERE " + PHONE_ACCOUNT_COMPONENT_NAME + " = ?"
+ " AND " + PHONE_ACCOUNT_ID + " = ?"
+ " ORDER BY " + DEFAULT_SORT_ORDER
@@ -2001,14 +2002,15 @@ public class CallLog {
});
} else {
// No valid phone account specified, so default to the old behavior.
- resolver.delete(uri, "_id IN " +
- "(SELECT _id FROM calls ORDER BY " + DEFAULT_SORT_ORDER
+ numDeleted = resolver.delete(uri, "_id IN "
+ + "(SELECT _id FROM calls ORDER BY " + DEFAULT_SORT_ORDER
+ " LIMIT -1 OFFSET 500)", null);
}
+ Log.i(LOG_TAG, "addEntry: cleaned up " + numDeleted + " old entries");
return result;
} catch (IllegalArgumentException e) {
- Log.w(LOG_TAG, "Failed to insert calllog", e);
+ Log.e(LOG_TAG, "Failed to insert calllog", e);
// Even though we make sure the target user is running and decrypted before calling
// this method, there's a chance that the user just got shut down, in which case
// we'll still get "IllegalArgumentException: Unknown URL content://call_log/calls".
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 9d2bb58cdf0f..2f12fecb7fd0 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -311,6 +311,16 @@ public final class ViewRootImpl implements ViewParent,
SystemProperties.getBoolean("persist.wm.debug.client_transient", false);
/**
+ * Whether the client (system UI) is handling the immersive confirmation window. If
+ * {@link CLIENT_TRANSIENT} is set to true, the immersive confirmation window will always be the
+ * client instance and this flag will be ignored. Otherwise, the immersive confirmation window
+ * can be switched freely by this flag.
+ * @hide
+ */
+ public static final boolean CLIENT_IMMERSIVE_CONFIRMATION =
+ SystemProperties.getBoolean("persist.wm.debug.client_immersive_confirmation", false);
+
+ /**
* Whether the client should compute the window frame on its own.
* @hide
*/
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index d5f2aa3b3631..65677cd7c3fd 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -3099,6 +3099,16 @@ public interface WindowManager extends ViewManager {
public static final int PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE = 1 << 16;
/**
+ * Flag to indicate that this window is a immersive mode confirmation window. The window
+ * should be ignored when calculating insets control. This is used for prompt window
+ * triggered by insets visibility changes. If it can take over the insets control, the
+ * visibility will change unexpectedly and the window may dismiss itself. Power button panic
+ * handling will be disabled when this window exists.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW = 1 << 17;
+
+ /**
* Flag to indicate that any window added by an application process that is of type
* {@link #TYPE_TOAST} or that requires
* {@link android.app.AppOpsManager#OP_SYSTEM_ALERT_WINDOW} permission should be hidden when
@@ -3242,6 +3252,7 @@ public interface WindowManager extends ViewManager {
PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME,
PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS,
PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE,
+ PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW,
SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY,
PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION,
@@ -3326,6 +3337,10 @@ public interface WindowManager extends ViewManager {
equals = PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE,
name = "SUSTAINED_PERFORMANCE_MODE"),
@ViewDebug.FlagToString(
+ mask = PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW,
+ equals = PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW,
+ name = "IMMERSIVE_CONFIRMATION_WINDOW"),
+ @ViewDebug.FlagToString(
mask = SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
equals = SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
name = "HIDE_NON_SYSTEM_OVERLAY_WINDOWS"),
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 50f393b53277..2445daf89b64 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -31,6 +31,7 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERS
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PROFILE_NOT_SUPPORTED;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB_ACCESSIBILITY;
+import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.PermissionChecker.PID_UNKNOWN;
import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
@@ -356,6 +357,12 @@ public class ResolverActivity extends Activity implements
// flag set, we are now losing it. That should be a very rare case
// and we can live with this.
intent.setFlags(intent.getFlags()&~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+
+ // If FLAG_ACTIVITY_LAUNCH_ADJACENT was set, ResolverActivity was opened in the alternate
+ // side, which means we want to open the target app on the same side as ResolverActivity.
+ if ((intent.getFlags() & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0) {
+ intent.setFlags(intent.getFlags() & ~FLAG_ACTIVITY_LAUNCH_ADJACENT);
+ }
return intent;
}
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index d2564fb9c268..c6f5086b8346 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -63,6 +63,19 @@ oneway interface IStatusBar
void cancelPreloadRecentApps();
void showScreenPinningRequest(int taskId);
+ /**
+ * Notify system UI the immersive prompt should be dismissed as confirmed, and the confirmed
+ * status should be saved without user clicking on the button. This could happen when a user
+ * swipe on the edge with the confirmation prompt showing.
+ */
+ void confirmImmersivePrompt();
+
+ /**
+ * Notify system UI the immersive mode changed. This shall be removed when client immersive is
+ * enabled.
+ */
+ void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode);
+
void dismissKeyboardShortcutsMenu();
void toggleKeyboardShortcutsMenu(int deviceId);
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index fc75ea4af0c6..ec03b8afd5cb 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2694,6 +2694,9 @@
backlight values -->
<bool name="config_displayBrightnessBucketsInDoze">false</bool>
+ <!-- True to skip the fade animation on display off event -->
+ <bool name="config_displayColorFadeDisabled">false</bool>
+
<!-- Power Management: Specifies whether to decouple the auto-suspend state of the
device from the display on/off state.
@@ -5718,6 +5721,9 @@
<!-- Whether per-app user aspect ratio override settings is enabled -->
<bool name="config_appCompatUserAppAspectRatioSettingsIsEnabled">false</bool>
+ <!-- Whether per-app fullscreen override option is allowed in user aspect ratio settings -->
+ <bool name="config_appCompatUserAppAspectRatioFullscreenIsEnabled">false</bool>
+
<!-- Whether sending compat fake focus for split screen resumed activities is enabled.
Needed because some game engines wait to get focus before drawing the content of
the app which isn't guaranteed by default in multi-window modes. -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index af203d3e566a..08c404b40ccc 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3855,6 +3855,7 @@
<java-symbol type="bool" name="config_dozeSupportsAodWallpaper" />
<java-symbol type="bool" name="config_displayBlanksAfterDoze" />
<java-symbol type="bool" name="config_displayBrightnessBucketsInDoze" />
+ <java-symbol type="bool" name="config_displayColorFadeDisabled" />
<java-symbol type="integer" name="config_storageManagerDaystoRetainDefault" />
<java-symbol type="string" name="config_headlineFontFamily" />
<java-symbol type="string" name="config_headlineFontFamilyMedium" />
@@ -4536,6 +4537,7 @@
<!-- Whether per-app user aspect ratio override settings is enabled -->
<java-symbol type="bool" name="config_appCompatUserAppAspectRatioSettingsIsEnabled" />
+ <java-symbol type="bool" name="config_appCompatUserAppAspectRatioFullscreenIsEnabled" />
<java-symbol type="bool" name="config_isCompatFakeFocusEnabled" />
<java-symbol type="bool" name="config_isWindowManagerCameraCompatTreatmentEnabled" />
diff --git a/core/tests/coretests/src/android/os/AidlTest.java b/core/tests/coretests/src/android/os/AidlTest.java
index 5f54b093e5e5..d0c3470c4c1f 100644
--- a/core/tests/coretests/src/android/os/AidlTest.java
+++ b/core/tests/coretests/src/android/os/AidlTest.java
@@ -28,12 +28,14 @@ public class AidlTest extends TestCase {
private IAidlTest mRemote;
private AidlObject mLocal;
+ private NonAutoGeneratedObject mNonAutoGenerated;
@Override
protected void setUp() throws Exception {
super.setUp();
mLocal = new AidlObject();
mRemote = IAidlTest.Stub.asInterface(mLocal);
+ mNonAutoGenerated = new NonAutoGeneratedObject("NonAutoGeneratedObject");
}
private static boolean check(TestParcelable p, int n, String s) {
@@ -84,6 +86,12 @@ public class AidlTest extends TestCase {
}
}
+ private static class NonAutoGeneratedObject extends Binder {
+ NonAutoGeneratedObject(String descriptor) {
+ super(descriptor);
+ }
+ }
+
private static class AidlObject extends IAidlTest.Stub {
public IInterface queryLocalInterface(String descriptor) {
// overriding this to return null makes asInterface always
@@ -194,7 +202,7 @@ public class AidlTest extends TestCase {
TestParcelable[] a1, TestParcelable[] a2) {
return null;
}
-
+
public void voidSecurityException() {
throw new SecurityException("gotcha!");
}
@@ -396,7 +404,7 @@ public class AidlTest extends TestCase {
assertEquals("s2[1]", s2[1]);
assertEquals("s2[2]", s2[2]);
}
-
+
@SmallTest
public void testVoidSecurityException() throws Exception {
boolean good = false;
@@ -407,7 +415,7 @@ public class AidlTest extends TestCase {
}
assertEquals(good, true);
}
-
+
@SmallTest
public void testIntSecurityException() throws Exception {
boolean good = false;
@@ -420,7 +428,7 @@ public class AidlTest extends TestCase {
}
@SmallTest
- public void testGetTransactionName() throws Exception {
+ public void testGetTransactionNameAutoGenerated() throws Exception {
assertEquals(15, mLocal.getMaxTransactionId());
assertEquals("booleanArray",
@@ -430,12 +438,21 @@ public class AidlTest extends TestCase {
assertEquals("parcelableIn",
mLocal.getTransactionName(IAidlTest.Stub.TRANSACTION_parcelableIn));
- assertEquals("IAidlTest:booleanArray",
+ assertEquals("AIDL::java::IAidlTest::booleanArray::server",
mLocal.getTransactionTraceName(IAidlTest.Stub.TRANSACTION_booleanArray));
- assertEquals("IAidlTest:voidSecurityException",
+ assertEquals("AIDL::java::IAidlTest::voidSecurityException::server",
mLocal.getTransactionTraceName(IAidlTest.Stub.TRANSACTION_voidSecurityException));
- assertEquals("IAidlTest:parcelableIn",
+ assertEquals("AIDL::java::IAidlTest::parcelableIn::server",
mLocal.getTransactionTraceName(IAidlTest.Stub.TRANSACTION_parcelableIn));
}
-}
+ @SmallTest
+ public void testGetTransactionNameNonAutoGenerated() throws Exception {
+ assertEquals(0, mNonAutoGenerated.getMaxTransactionId());
+
+ assertEquals("AIDL::java::NonAutoGeneratedObject::#0::server",
+ mNonAutoGenerated.getTransactionTraceName(0));
+ assertEquals("AIDL::java::NonAutoGeneratedObject::#1::server",
+ mNonAutoGenerated.getTransactionTraceName(1));
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
index 39f861de1ba0..5cf9175073c0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
@@ -50,7 +50,7 @@ class ActivityEmbeddingAnimationAdapter {
final SurfaceControl mLeash;
/** Area in absolute coordinate that the animation surface shouldn't go beyond. */
@NonNull
- private final Rect mWholeAnimationBounds = new Rect();
+ final Rect mWholeAnimationBounds = new Rect();
/**
* Area in absolute coordinate that should represent all the content to show for this window.
* This should be the end bounds for opening window, and start bounds for closing window in case
@@ -229,20 +229,7 @@ class ActivityEmbeddingAnimationAdapter {
mTransformation.getMatrix().postTranslate(mContentRelOffset.x, mContentRelOffset.y);
t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
t.setAlpha(mLeash, mTransformation.getAlpha());
-
- // The following applies an inverse scale to the clip-rect so that it crops "after" the
- // scale instead of before.
- mVecs[1] = mVecs[2] = 0;
- mVecs[0] = mVecs[3] = 1;
- mTransformation.getMatrix().mapVectors(mVecs);
- mVecs[0] = 1.f / mVecs[0];
- mVecs[3] = 1.f / mVecs[3];
- final Rect clipRect = mTransformation.getClipRect();
- mRect.left = (int) (clipRect.left * mVecs[0] + 0.5f);
- mRect.right = (int) (clipRect.right * mVecs[0] + 0.5f);
- mRect.top = (int) (clipRect.top * mVecs[3] + 0.5f);
- mRect.bottom = (int) (clipRect.bottom * mVecs[3] + 0.5f);
- t.setCrop(mLeash, mRect);
+ t.setWindowCrop(mLeash, mWholeAnimationBounds.width(), mWholeAnimationBounds.height());
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
index 1793a3d0feb4..4640106b5f1c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
@@ -26,7 +26,6 @@ import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.AnimationUtils;
-import android.view.animation.ClipRectAnimation;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.view.animation.ScaleAnimation;
@@ -189,14 +188,6 @@ class ActivityEmbeddingAnimationSpec {
startBounds.top - endBounds.top, 0);
endTranslate.setDuration(CHANGE_ANIMATION_DURATION);
endSet.addAnimation(endTranslate);
- // The end leash is resizing, we should update the window crop based on the clip rect.
- final Rect startClip = new Rect(startBounds);
- final Rect endClip = new Rect(endBounds);
- startClip.offsetTo(0, 0);
- endClip.offsetTo(0, 0);
- final Animation clipAnim = new ClipRectAnimation(startClip, endClip);
- clipAnim.setDuration(CHANGE_ANIMATION_DURATION);
- endSet.addAnimation(clipAnim);
endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(),
parentBounds.height());
endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
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 65727b6145e4..51e7be0f8a24 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
@@ -939,6 +939,11 @@ public class PipController implements PipTransitionController.PipTransitionCallb
* Sets both shelf visibility and its height.
*/
private void setShelfHeight(boolean visible, int height) {
+ if (mEnablePipKeepClearAlgorithm) {
+ // turn this into Launcher keep clear area registration instead
+ setLauncherKeepClearAreaHeight(visible, height);
+ return;
+ }
if (!mIsKeyguardShowingOrAnimating) {
setShelfHeightLocked(visible, height);
}
diff --git a/media/java/android/media/audiofx/Visualizer.java b/media/java/android/media/audiofx/Visualizer.java
index f3dfeb665cc7..2795cfe4ba61 100644
--- a/media/java/android/media/audiofx/Visualizer.java
+++ b/media/java/android/media/audiofx/Visualizer.java
@@ -455,11 +455,13 @@ public class Visualizer {
* a number of consecutive 8-bit (unsigned) mono PCM samples equal to the capture size returned
* by {@link #getCaptureSize()}.
* <p>This method must be called when the Visualizer is enabled.
- * @param waveform array of bytes where the waveform should be returned
+ * @param waveform array of bytes where the waveform should be returned, array length must be
+ * at least equals to the capture size returned by {@link #getCaptureSize()}.
* @return {@link #SUCCESS} in case of success,
* {@link #ERROR_NO_MEMORY}, {@link #ERROR_INVALID_OPERATION} or {@link #ERROR_DEAD_OBJECT}
* in case of failure.
* @throws IllegalStateException
+ * @throws IllegalArgumentException
*/
public int getWaveForm(byte[] waveform)
throws IllegalStateException {
@@ -467,6 +469,12 @@ public class Visualizer {
if (mState != STATE_ENABLED) {
throw(new IllegalStateException("getWaveForm() called in wrong state: "+mState));
}
+ int captureSize = getCaptureSize();
+ if (captureSize > waveform.length) {
+ throw(new IllegalArgumentException("getWaveForm() called with illegal size: "
+ + waveform.length + " expecting at least "
+ + captureSize + " bytes"));
+ }
return native_getWaveForm(waveform);
}
}
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 7be60431b91b..e2599a3583c7 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -228,6 +228,17 @@ filegroup {
}
filegroup {
+ name: "SystemUI-test-fakes",
+ srcs: [
+ /* Status bar fakes */
+ "tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt",
+ "tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt",
+ "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt",
+ ],
+ path: "tests/src",
+}
+
+filegroup {
name: "SystemUI-tests-robolectric-pilots",
srcs: [
/* Keyguard converted tests */
@@ -291,6 +302,11 @@ filegroup {
"tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt",
"tests/src/com/android/systemui/biometrics/UdfpsShellTest.kt",
"tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt",
+
+ /* Status bar wifi converted tests */
+ "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt",
+ "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt",
+ "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt",
],
path: "tests/src",
}
@@ -449,6 +465,7 @@ android_robolectric_test {
"tests/robolectric/src/**/*.kt",
"tests/robolectric/src/**/*.java",
":SystemUI-tests-utils",
+ ":SystemUI-test-fakes",
":SystemUI-tests-robolectric-pilots",
],
static_libs: [
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
index dbfa192f5ec4..37b1ee543e46 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -32,11 +32,10 @@ import android.view.ViewRootImpl
import android.view.WindowInsets
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
-import android.widget.FrameLayout
import com.android.app.animation.Interpolators
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.jank.InteractionJankMonitor.CujType
-import com.android.systemui.animation.view.LaunchableFrameLayout
+import com.android.systemui.util.maybeForceFullscreen
import com.android.systemui.util.registerAnimationOnBackInvoked
import kotlin.math.roundToInt
@@ -622,96 +621,12 @@ private class AnimatedDialog(
viewGroupWithBackground
} else {
- // We will make the dialog window (and therefore its DecorView) fullscreen to make
- // it possible to animate outside its bounds.
- //
- // Before that, we add a new View as a child of the DecorView with the same size and
- // gravity as that DecorView, then we add all original children of the DecorView to
- // that new View. Finally we remove the background of the DecorView and add it to
- // the new View, then we make the DecorView fullscreen. This new View now acts as a
- // fake (non fullscreen) window.
- //
- // On top of that, we also add a fullscreen transparent background between the
- // DecorView and the view that we added so that we can dismiss the dialog when this
- // view is clicked. This is necessary because DecorView overrides onTouchEvent and
- // therefore we can't set the click listener directly on the (now fullscreen)
- // DecorView.
- val fullscreenTransparentBackground = FrameLayout(dialog.context)
- decorView.addView(
- fullscreenTransparentBackground,
- 0 /* index */,
- FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
- )
-
- val dialogContentWithBackground = LaunchableFrameLayout(dialog.context)
- dialogContentWithBackground.background = decorView.background
-
- // Make the window background transparent. Note that setting the window (or
- // DecorView) background drawable to null leads to issues with background color (not
- // being transparent) or with insets that are not refreshed. Therefore we need to
- // set it to something not null, hence we are using android.R.color.transparent
- // here.
- window.setBackgroundDrawableResource(android.R.color.transparent)
-
- // Close the dialog when clicking outside of it.
- fullscreenTransparentBackground.setOnClickListener { dialog.dismiss() }
- dialogContentWithBackground.isClickable = true
-
- // Make sure the transparent and dialog backgrounds are not focusable by
- // accessibility
- // features.
- fullscreenTransparentBackground.importantForAccessibility =
- View.IMPORTANT_FOR_ACCESSIBILITY_NO
- dialogContentWithBackground.importantForAccessibility =
- View.IMPORTANT_FOR_ACCESSIBILITY_NO
-
- fullscreenTransparentBackground.addView(
- dialogContentWithBackground,
- FrameLayout.LayoutParams(
- window.attributes.width,
- window.attributes.height,
- window.attributes.gravity
- )
- )
-
- // Move all original children of the DecorView to the new View we just added.
- for (i in 1 until decorView.childCount) {
- val view = decorView.getChildAt(1)
- decorView.removeViewAt(1)
- dialogContentWithBackground.addView(view)
- }
-
- // Make the window fullscreen and add a layout listener to ensure it stays
- // fullscreen.
- window.setLayout(MATCH_PARENT, MATCH_PARENT)
- decorViewLayoutListener =
- View.OnLayoutChangeListener {
- v,
- left,
- top,
- right,
- bottom,
- oldLeft,
- oldTop,
- oldRight,
- oldBottom ->
- if (
- window.attributes.width != MATCH_PARENT ||
- window.attributes.height != MATCH_PARENT
- ) {
- // The dialog size changed, copy its size to dialogContentWithBackground
- // and make the dialog window full screen again.
- val layoutParams = dialogContentWithBackground.layoutParams
- layoutParams.width = window.attributes.width
- layoutParams.height = window.attributes.height
- dialogContentWithBackground.layoutParams = layoutParams
- window.setLayout(MATCH_PARENT, MATCH_PARENT)
- }
- }
- decorView.addOnLayoutChangeListener(decorViewLayoutListener)
-
+ val (dialogContentWithBackground, decorViewLayoutListener) =
+ dialog.maybeForceFullscreen()!!
+ this.decorViewLayoutListener = decorViewLayoutListener
dialogContentWithBackground
}
+
this.dialogContentWithBackground = dialogContentWithBackground
dialogContentWithBackground.setTag(R.id.tag_dialog_background, true)
diff --git a/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt b/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt
index 428856dc5f30..0f63548b6f0c 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt
@@ -18,6 +18,9 @@ package com.android.systemui.util
import android.app.Dialog
import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.widget.FrameLayout
import android.window.OnBackInvokedDispatcher
import com.android.systemui.animation.back.BackAnimationSpec
import com.android.systemui.animation.back.BackTransformation
@@ -25,6 +28,7 @@ import com.android.systemui.animation.back.applyTo
import com.android.systemui.animation.back.floatingSystemSurfacesForSysUi
import com.android.systemui.animation.back.onBackAnimationCallbackFrom
import com.android.systemui.animation.back.registerOnBackInvokedCallbackOnViewAttached
+import com.android.systemui.animation.view.LaunchableFrameLayout
/**
* Register on the Dialog's [OnBackInvokedDispatcher] an animation using the [BackAnimationSpec].
@@ -49,3 +53,110 @@ fun Dialog.registerAnimationOnBackInvoked(
),
)
}
+
+/**
+ * Make the dialog window (and therefore its DecorView) fullscreen to make it possible to animate
+ * outside its bounds. No-op if the dialog is already fullscreen.
+ *
+ * <p>Returns null if the dialog is already fullscreen. Otherwise, returns a pair containing a view
+ * and a layout listener. The new view matches the original dialog DecorView in size, position, and
+ * background. This new view will be a child of the modified, transparent, fullscreen DecorView. The
+ * layout listener is listening to changes to the modified DecorView. It is the responsibility of
+ * the caller to deregister the listener when the dialog is dismissed.
+ */
+fun Dialog.maybeForceFullscreen(): Pair<LaunchableFrameLayout, View.OnLayoutChangeListener>? {
+ // Create the dialog so that its onCreate() method is called, which usually sets the dialog
+ // content.
+ create()
+
+ val window = window!!
+ val decorView = window.decorView as ViewGroup
+
+ val isWindowFullscreen =
+ window.attributes.width == MATCH_PARENT && window.attributes.height == MATCH_PARENT
+ if (isWindowFullscreen) {
+ return null
+ }
+
+ // We will make the dialog window (and therefore its DecorView) fullscreen to make it possible
+ // to animate outside its bounds.
+ //
+ // Before that, we add a new View as a child of the DecorView with the same size and gravity as
+ // that DecorView, then we add all original children of the DecorView to that new View. Finally
+ // we remove the background of the DecorView and add it to the new View, then we make the
+ // DecorView fullscreen. This new View now acts as a fake (non fullscreen) window.
+ //
+ // On top of that, we also add a fullscreen transparent background between the DecorView and the
+ // view that we added so that we can dismiss the dialog when this view is clicked. This is
+ // necessary because DecorView overrides onTouchEvent and therefore we can't set the click
+ // listener directly on the (now fullscreen) DecorView.
+ val fullscreenTransparentBackground = FrameLayout(context)
+ decorView.addView(
+ fullscreenTransparentBackground,
+ 0 /* index */,
+ FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
+ )
+
+ val dialogContentWithBackground = LaunchableFrameLayout(context)
+ dialogContentWithBackground.background = decorView.background
+
+ // Make the window background transparent. Note that setting the window (or DecorView)
+ // background drawable to null leads to issues with background color (not being transparent) or
+ // with insets that are not refreshed. Therefore we need to set it to something not null, hence
+ // we are using android.R.color.transparent here.
+ window.setBackgroundDrawableResource(android.R.color.transparent)
+
+ // Close the dialog when clicking outside of it.
+ fullscreenTransparentBackground.setOnClickListener { dismiss() }
+ dialogContentWithBackground.isClickable = true
+
+ // Make sure the transparent and dialog backgrounds are not focusable by accessibility
+ // features.
+ fullscreenTransparentBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
+ dialogContentWithBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
+
+ fullscreenTransparentBackground.addView(
+ dialogContentWithBackground,
+ FrameLayout.LayoutParams(
+ window.attributes.width,
+ window.attributes.height,
+ window.attributes.gravity
+ )
+ )
+
+ // Move all original children of the DecorView to the new View we just added.
+ for (i in 1 until decorView.childCount) {
+ val view = decorView.getChildAt(1)
+ decorView.removeViewAt(1)
+ dialogContentWithBackground.addView(view)
+ }
+
+ // Make the window fullscreen and add a layout listener to ensure it stays fullscreen.
+ window.setLayout(MATCH_PARENT, MATCH_PARENT)
+ val decorViewLayoutListener =
+ View.OnLayoutChangeListener {
+ v,
+ left,
+ top,
+ right,
+ bottom,
+ oldLeft,
+ oldTop,
+ oldRight,
+ oldBottom ->
+ if (
+ window.attributes.width != MATCH_PARENT || window.attributes.height != MATCH_PARENT
+ ) {
+ // The dialog size changed, copy its size to dialogContentWithBackground and make
+ // the dialog window full screen again.
+ val layoutParams = dialogContentWithBackground.layoutParams
+ layoutParams.width = window.attributes.width
+ layoutParams.height = window.attributes.height
+ dialogContentWithBackground.layoutParams = layoutParams
+ window.setLayout(MATCH_PARENT, MATCH_PARENT)
+ }
+ }
+ decorView.addOnLayoutChangeListener(decorViewLayoutListener)
+
+ return dialogContentWithBackground to decorViewLayoutListener
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/pager/Pager.kt b/packages/SystemUI/compose/core/src/com/android/compose/pager/Pager.kt
deleted file mode 100644
index a80a1f934dab..000000000000
--- a/packages/SystemUI/compose/core/src/com/android/compose/pager/Pager.kt
+++ /dev/null
@@ -1,357 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.compose.pager
-
-import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.DecayAnimationSpec
-import androidx.compose.animation.rememberSplineBasedDecay
-import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
-import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.Velocity
-import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.flow.filter
-
-/** Library-wide switch to turn on debug logging. */
-internal const val DebugLog = false
-
-@RequiresOptIn(message = "Accompanist Pager is experimental. The API may be changed in the future.")
-@Retention(AnnotationRetention.BINARY)
-annotation class ExperimentalPagerApi
-
-/** Contains the default values used by [HorizontalPager] and [VerticalPager]. */
-@ExperimentalPagerApi
-object PagerDefaults {
- /**
- * Remember the default [FlingBehavior] that represents the scroll curve.
- *
- * @param state The [PagerState] to update.
- * @param decayAnimationSpec The decay animation spec to use for decayed flings.
- * @param snapAnimationSpec The animation spec to use when snapping.
- */
- @Composable
- fun flingBehavior(
- state: PagerState,
- decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
- snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec,
- ): FlingBehavior =
- rememberSnappingFlingBehavior(
- lazyListState = state.lazyListState,
- decayAnimationSpec = decayAnimationSpec,
- snapAnimationSpec = snapAnimationSpec,
- )
-
- @Deprecated(
- "Replaced with PagerDefaults.flingBehavior()",
- ReplaceWith("PagerDefaults.flingBehavior(state, decayAnimationSpec, snapAnimationSpec)")
- )
- @Composable
- fun rememberPagerFlingConfig(
- state: PagerState,
- decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
- snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec,
- ): FlingBehavior = flingBehavior(state, decayAnimationSpec, snapAnimationSpec)
-}
-
-/**
- * A horizontally scrolling layout that allows users to flip between items to the left and right.
- *
- * @param count the number of pages.
- * @param modifier the modifier to apply to this layout.
- * @param state the state object to be used to control or observe the pager's state.
- * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
- * composed from the end to the start and [PagerState.currentPage] == 0 will mean the first item
- * is located at the end.
- * @param itemSpacing horizontal spacing to add between items.
- * @param flingBehavior logic describing fling behavior.
- * @param key the scroll position will be maintained based on the key, which means if you add/remove
- * items before the current visible item the item with the given key will be kept as the first
- * visible one.
- * @param content a block which describes the content. Inside this block you can reference
- * [PagerScope.currentPage] and other properties in [PagerScope].
- * @sample com.google.accompanist.sample.pager.HorizontalPagerSample
- */
-@ExperimentalPagerApi
-@Composable
-fun HorizontalPager(
- count: Int,
- modifier: Modifier = Modifier,
- state: PagerState = rememberPagerState(),
- reverseLayout: Boolean = false,
- itemSpacing: Dp = 0.dp,
- flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(state),
- verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
- key: ((page: Int) -> Any)? = null,
- contentPadding: PaddingValues = PaddingValues(0.dp),
- content: @Composable PagerScope.(page: Int) -> Unit,
-) {
- Pager(
- count = count,
- state = state,
- modifier = modifier,
- isVertical = false,
- reverseLayout = reverseLayout,
- itemSpacing = itemSpacing,
- verticalAlignment = verticalAlignment,
- flingBehavior = flingBehavior,
- key = key,
- contentPadding = contentPadding,
- content = content
- )
-}
-
-/**
- * A vertically scrolling layout that allows users to flip between items to the top and bottom.
- *
- * @param count the number of pages.
- * @param modifier the modifier to apply to this layout.
- * @param state the state object to be used to control or observe the pager's state.
- * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
- * composed from the bottom to the top and [PagerState.currentPage] == 0 will mean the first item
- * is located at the bottom.
- * @param itemSpacing vertical spacing to add between items.
- * @param flingBehavior logic describing fling behavior.
- * @param key the scroll position will be maintained based on the key, which means if you add/remove
- * items before the current visible item the item with the given key will be kept as the first
- * visible one.
- * @param content a block which describes the content. Inside this block you can reference
- * [PagerScope.currentPage] and other properties in [PagerScope].
- * @sample com.google.accompanist.sample.pager.VerticalPagerSample
- */
-@ExperimentalPagerApi
-@Composable
-fun VerticalPager(
- count: Int,
- modifier: Modifier = Modifier,
- state: PagerState = rememberPagerState(),
- reverseLayout: Boolean = false,
- itemSpacing: Dp = 0.dp,
- flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(state),
- horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
- key: ((page: Int) -> Any)? = null,
- contentPadding: PaddingValues = PaddingValues(0.dp),
- content: @Composable PagerScope.(page: Int) -> Unit,
-) {
- Pager(
- count = count,
- state = state,
- modifier = modifier,
- isVertical = true,
- reverseLayout = reverseLayout,
- itemSpacing = itemSpacing,
- horizontalAlignment = horizontalAlignment,
- flingBehavior = flingBehavior,
- key = key,
- contentPadding = contentPadding,
- content = content
- )
-}
-
-@ExperimentalPagerApi
-@Composable
-internal fun Pager(
- count: Int,
- modifier: Modifier,
- state: PagerState,
- reverseLayout: Boolean,
- itemSpacing: Dp,
- isVertical: Boolean,
- flingBehavior: FlingBehavior,
- key: ((page: Int) -> Any)?,
- contentPadding: PaddingValues,
- verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
- horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
- content: @Composable PagerScope.(page: Int) -> Unit,
-) {
- require(count >= 0) { "pageCount must be >= 0" }
-
- // Provide our PagerState with access to the SnappingFlingBehavior animation target
- // TODO: can this be done in a better way?
- state.flingAnimationTarget = { (flingBehavior as? SnappingFlingBehavior)?.animationTarget }
-
- LaunchedEffect(count) {
- state.currentPage = minOf(count - 1, state.currentPage).coerceAtLeast(0)
- }
-
- // Once a fling (scroll) has finished, notify the state
- LaunchedEffect(state) {
- // When a 'scroll' has finished, notify the state
- snapshotFlow { state.isScrollInProgress }
- .filter { !it }
- .collect { state.onScrollFinished() }
- }
-
- val pagerScope = remember(state) { PagerScopeImpl(state) }
-
- // We only consume nested flings in the main-axis, allowing cross-axis flings to propagate
- // as normal
- val consumeFlingNestedScrollConnection =
- ConsumeFlingNestedScrollConnection(
- consumeHorizontal = !isVertical,
- consumeVertical = isVertical,
- )
-
- if (isVertical) {
- LazyColumn(
- state = state.lazyListState,
- verticalArrangement = Arrangement.spacedBy(itemSpacing, verticalAlignment),
- horizontalAlignment = horizontalAlignment,
- flingBehavior = flingBehavior,
- reverseLayout = reverseLayout,
- contentPadding = contentPadding,
- modifier = modifier,
- ) {
- items(
- count = count,
- key = key,
- ) { page ->
- Box(
- Modifier
- // We don't any nested flings to continue in the pager, so we add a
- // connection which consumes them.
- // See: https://github.com/google/accompanist/issues/347
- .nestedScroll(connection = consumeFlingNestedScrollConnection)
- // Constraint the content to be <= than the size of the pager.
- .fillParentMaxHeight()
- .wrapContentSize()
- ) {
- pagerScope.content(page)
- }
- }
- }
- } else {
- LazyRow(
- state = state.lazyListState,
- verticalAlignment = verticalAlignment,
- horizontalArrangement = Arrangement.spacedBy(itemSpacing, horizontalAlignment),
- flingBehavior = flingBehavior,
- reverseLayout = reverseLayout,
- contentPadding = contentPadding,
- modifier = modifier,
- ) {
- items(
- count = count,
- key = key,
- ) { page ->
- Box(
- Modifier
- // We don't any nested flings to continue in the pager, so we add a
- // connection which consumes them.
- // See: https://github.com/google/accompanist/issues/347
- .nestedScroll(connection = consumeFlingNestedScrollConnection)
- // Constraint the content to be <= than the size of the pager.
- .fillParentMaxWidth()
- .wrapContentSize()
- ) {
- pagerScope.content(page)
- }
- }
- }
- }
-}
-
-private class ConsumeFlingNestedScrollConnection(
- private val consumeHorizontal: Boolean,
- private val consumeVertical: Boolean,
-) : NestedScrollConnection {
- override fun onPostScroll(
- consumed: Offset,
- available: Offset,
- source: NestedScrollSource
- ): Offset =
- when (source) {
- // We can consume all resting fling scrolls so that they don't propagate up to the
- // Pager
- NestedScrollSource.Fling -> available.consume(consumeHorizontal, consumeVertical)
- else -> Offset.Zero
- }
-
- override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
- // We can consume all post fling velocity on the main-axis
- // so that it doesn't propagate up to the Pager
- return available.consume(consumeHorizontal, consumeVertical)
- }
-}
-
-private fun Offset.consume(
- consumeHorizontal: Boolean,
- consumeVertical: Boolean,
-): Offset =
- Offset(
- x = if (consumeHorizontal) this.x else 0f,
- y = if (consumeVertical) this.y else 0f,
- )
-
-private fun Velocity.consume(
- consumeHorizontal: Boolean,
- consumeVertical: Boolean,
-): Velocity =
- Velocity(
- x = if (consumeHorizontal) this.x else 0f,
- y = if (consumeVertical) this.y else 0f,
- )
-
-/** Scope for [HorizontalPager] content. */
-@ExperimentalPagerApi
-@Stable
-interface PagerScope {
- /** Returns the current selected page */
- val currentPage: Int
-
- /** The current offset from the start of [currentPage], as a ratio of the page width. */
- val currentPageOffset: Float
-}
-
-@ExperimentalPagerApi
-private class PagerScopeImpl(
- private val state: PagerState,
-) : PagerScope {
- override val currentPage: Int
- get() = state.currentPage
- override val currentPageOffset: Float
- get() = state.currentPageOffset
-}
-
-/**
- * Calculate the offset for the given [page] from the current scroll position. This is useful when
- * using the scroll position to apply effects or animations to items.
- *
- * The returned offset can positive or negative, depending on whether which direction the [page] is
- * compared to the current scroll position.
- *
- * @sample com.google.accompanist.sample.pager.HorizontalPagerWithOffsetTransition
- */
-@ExperimentalPagerApi
-fun PagerScope.calculateCurrentOffsetForPage(page: Int): Float {
- return (currentPage + currentPageOffset) - page
-}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/pager/PagerState.kt b/packages/SystemUI/compose/core/src/com/android/compose/pager/PagerState.kt
deleted file mode 100644
index 1822a68f1e77..000000000000
--- a/packages/SystemUI/compose/core/src/com/android/compose/pager/PagerState.kt
+++ /dev/null
@@ -1,348 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.compose.pager
-
-import androidx.annotation.FloatRange
-import androidx.annotation.IntRange
-import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.spring
-import androidx.compose.foundation.MutatePriority
-import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.foundation.gestures.ScrollableState
-import androidx.compose.foundation.interaction.InteractionSource
-import androidx.compose.foundation.lazy.LazyListItemInfo
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.Saver
-import androidx.compose.runtime.saveable.listSaver
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import kotlin.math.absoluteValue
-import kotlin.math.roundToInt
-
-@Deprecated(
- "Replaced with rememberPagerState(initialPage) and count parameter on Pager composables",
- ReplaceWith("rememberPagerState(initialPage)"),
- level = DeprecationLevel.ERROR,
-)
-@Suppress("UNUSED_PARAMETER", "NOTHING_TO_INLINE")
-@ExperimentalPagerApi
-@Composable
-inline fun rememberPagerState(
- @IntRange(from = 0) pageCount: Int,
- @IntRange(from = 0) initialPage: Int = 0,
- @FloatRange(from = 0.0, to = 1.0) initialPageOffset: Float = 0f,
- @IntRange(from = 1) initialOffscreenLimit: Int = 1,
- infiniteLoop: Boolean = false
-): PagerState {
- return rememberPagerState(initialPage = initialPage)
-}
-
-/**
- * Creates a [PagerState] that is remembered across compositions.
- *
- * Changes to the provided values for [initialPage] will **not** result in the state being recreated
- * or changed in any way if it has already been created.
- *
- * @param initialPage the initial value for [PagerState.currentPage]
- */
-@ExperimentalPagerApi
-@Composable
-fun rememberPagerState(
- @IntRange(from = 0) initialPage: Int = 0,
-): PagerState =
- rememberSaveable(saver = PagerState.Saver) {
- PagerState(
- currentPage = initialPage,
- )
- }
-
-/**
- * A state object that can be hoisted to control and observe scrolling for [HorizontalPager].
- *
- * In most cases, this will be created via [rememberPagerState].
- *
- * @param currentPage the initial value for [PagerState.currentPage]
- */
-@ExperimentalPagerApi
-@Stable
-class PagerState(
- @IntRange(from = 0) currentPage: Int = 0,
-) : ScrollableState {
- // Should this be public?
- internal val lazyListState = LazyListState(firstVisibleItemIndex = currentPage)
-
- private var _currentPage by mutableStateOf(currentPage)
-
- private val currentLayoutPageInfo: LazyListItemInfo?
- get() =
- lazyListState.layoutInfo.visibleItemsInfo
- .asSequence()
- .filter { it.offset <= 0 && it.offset + it.size > 0 }
- .lastOrNull()
-
- private val currentLayoutPageOffset: Float
- get() =
- currentLayoutPageInfo?.let { current ->
- // We coerce since itemSpacing can make the offset > 1f.
- // We don't want to count spacing in the offset so cap it to 1f
- (-current.offset / current.size.toFloat()).coerceIn(0f, 1f)
- }
- ?: 0f
-
- /**
- * [InteractionSource] that will be used to dispatch drag events when this list is being
- * dragged. If you want to know whether the fling (or animated scroll) is in progress, use
- * [isScrollInProgress].
- */
- val interactionSource: InteractionSource
- get() = lazyListState.interactionSource
-
- /** The number of pages to display. */
- @get:IntRange(from = 0)
- val pageCount: Int by derivedStateOf { lazyListState.layoutInfo.totalItemsCount }
-
- /**
- * The index of the currently selected page. This may not be the page which is currently
- * displayed on screen.
- *
- * To update the scroll position, use [scrollToPage] or [animateScrollToPage].
- */
- @get:IntRange(from = 0)
- var currentPage: Int
- get() = _currentPage
- internal set(value) {
- if (value != _currentPage) {
- _currentPage = value
- }
- }
-
- /**
- * The current offset from the start of [currentPage], as a ratio of the page width.
- *
- * To update the scroll position, use [scrollToPage] or [animateScrollToPage].
- */
- val currentPageOffset: Float by derivedStateOf {
- currentLayoutPageInfo?.let {
- // The current page offset is the current layout page delta from `currentPage`
- // (which is only updated after a scroll/animation).
- // We calculate this by looking at the current layout page + it's offset,
- // then subtracting the 'current page'.
- it.index + currentLayoutPageOffset - _currentPage
- }
- ?: 0f
- }
-
- /** The target page for any on-going animations. */
- private var animationTargetPage: Int? by mutableStateOf(null)
-
- internal var flingAnimationTarget: (() -> Int?)? by mutableStateOf(null)
-
- /**
- * The target page for any on-going animations or scrolls by the user. Returns the current page
- * if a scroll or animation is not currently in progress.
- */
- val targetPage: Int
- get() =
- animationTargetPage
- ?: flingAnimationTarget?.invoke()
- ?: when {
- // If a scroll isn't in progress, return the current page
- !isScrollInProgress -> currentPage
- // If the offset is 0f (or very close), return the current page
- currentPageOffset.absoluteValue < 0.001f -> currentPage
- // If we're offset towards the start, guess the previous page
- currentPageOffset < -0.5f -> (currentPage - 1).coerceAtLeast(0)
- // If we're offset towards the end, guess the next page
- else -> (currentPage + 1).coerceAtMost(pageCount - 1)
- }
-
- @Deprecated(
- "Replaced with animateScrollToPage(page, pageOffset)",
- ReplaceWith("animateScrollToPage(page = page, pageOffset = pageOffset)")
- )
- @Suppress("UNUSED_PARAMETER")
- suspend fun animateScrollToPage(
- @IntRange(from = 0) page: Int,
- @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
- animationSpec: AnimationSpec<Float> = spring(),
- initialVelocity: Float = 0f,
- skipPages: Boolean = true,
- ) {
- animateScrollToPage(page = page, pageOffset = pageOffset)
- }
-
- /**
- * Animate (smooth scroll) to the given page to the middle of the viewport.
- *
- * Cancels the currently running scroll, if any, and suspends until the cancellation is
- * complete.
- *
- * @param page the page to animate to. Must be between 0 and [pageCount] (inclusive).
- * @param pageOffset the percentage of the page width to offset, from the start of [page]. Must
- * be in the range 0f..1f.
- */
- suspend fun animateScrollToPage(
- @IntRange(from = 0) page: Int,
- @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
- ) {
- requireCurrentPage(page, "page")
- requireCurrentPageOffset(pageOffset, "pageOffset")
- try {
- animationTargetPage = page
-
- if (pageOffset <= 0.005f) {
- // If the offset is (close to) zero, just call animateScrollToItem and we're done
- lazyListState.animateScrollToItem(index = page)
- } else {
- // Else we need to figure out what the offset is in pixels...
-
- var target =
- lazyListState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == page }
-
- if (target != null) {
- // If we have access to the target page layout, we can calculate the pixel
- // offset from the size
- lazyListState.animateScrollToItem(
- index = page,
- scrollOffset = (target.size * pageOffset).roundToInt()
- )
- } else {
- // If we don't, we use the current page size as a guide
- val currentSize = currentLayoutPageInfo!!.size
- lazyListState.animateScrollToItem(
- index = page,
- scrollOffset = (currentSize * pageOffset).roundToInt()
- )
-
- // The target should be visible now
- target = lazyListState.layoutInfo.visibleItemsInfo.first { it.index == page }
-
- if (target.size != currentSize) {
- // If the size we used for calculating the offset differs from the actual
- // target page size, we need to scroll again. This doesn't look great,
- // but there's not much else we can do.
- lazyListState.animateScrollToItem(
- index = page,
- scrollOffset = (target.size * pageOffset).roundToInt()
- )
- }
- }
- }
- } finally {
- // We need to manually call this, as the `animateScrollToItem` call above will happen
- // in 1 frame, which is usually too fast for the LaunchedEffect in Pager to detect
- // the change. This is especially true when running unit tests.
- onScrollFinished()
- }
- }
-
- /**
- * Instantly brings the item at [page] to the middle of the viewport.
- *
- * Cancels the currently running scroll, if any, and suspends until the cancellation is
- * complete.
- *
- * @param page the page to snap to. Must be between 0 and [pageCount] (inclusive).
- */
- suspend fun scrollToPage(
- @IntRange(from = 0) page: Int,
- @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
- ) {
- requireCurrentPage(page, "page")
- requireCurrentPageOffset(pageOffset, "pageOffset")
- try {
- animationTargetPage = page
-
- // First scroll to the given page. It will now be laid out at offset 0
- lazyListState.scrollToItem(index = page)
-
- // If we have a start spacing, we need to offset (scroll) by that too
- if (pageOffset > 0.0001f) {
- scroll { currentLayoutPageInfo?.let { scrollBy(it.size * pageOffset) } }
- }
- } finally {
- // We need to manually call this, as the `scroll` call above will happen in 1 frame,
- // which is usually too fast for the LaunchedEffect in Pager to detect the change.
- // This is especially true when running unit tests.
- onScrollFinished()
- }
- }
-
- internal fun onScrollFinished() {
- // Then update the current page to our layout page
- currentPage = currentLayoutPageInfo?.index ?: 0
- // Clear the animation target page
- animationTargetPage = null
- }
-
- override suspend fun scroll(
- scrollPriority: MutatePriority,
- block: suspend ScrollScope.() -> Unit
- ) = lazyListState.scroll(scrollPriority, block)
-
- override fun dispatchRawDelta(delta: Float): Float {
- return lazyListState.dispatchRawDelta(delta)
- }
-
- override val isScrollInProgress: Boolean
- get() = lazyListState.isScrollInProgress
-
- override fun toString(): String =
- "PagerState(" +
- "pageCount=$pageCount, " +
- "currentPage=$currentPage, " +
- "currentPageOffset=$currentPageOffset" +
- ")"
-
- private fun requireCurrentPage(value: Int, name: String) {
- if (pageCount == 0) {
- require(value == 0) { "$name must be 0 when pageCount is 0" }
- } else {
- require(value in 0 until pageCount) { "$name[$value] must be >= 0 and < pageCount" }
- }
- }
-
- private fun requireCurrentPageOffset(value: Float, name: String) {
- if (pageCount == 0) {
- require(value == 0f) { "$name must be 0f when pageCount is 0" }
- } else {
- require(value in 0f..1f) { "$name must be >= 0 and <= 1" }
- }
- }
-
- companion object {
- /** The default [Saver] implementation for [PagerState]. */
- val Saver: Saver<PagerState, *> =
- listSaver(
- save = {
- listOf<Any>(
- it.currentPage,
- )
- },
- restore = {
- PagerState(
- currentPage = it[0] as Int,
- )
- }
- )
- }
-}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/pager/SnappingFlingBehavior.kt b/packages/SystemUI/compose/core/src/com/android/compose/pager/SnappingFlingBehavior.kt
deleted file mode 100644
index 98140295306a..000000000000
--- a/packages/SystemUI/compose/core/src/com/android/compose/pager/SnappingFlingBehavior.kt
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.compose.pager
-
-import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.AnimationState
-import androidx.compose.animation.core.DecayAnimationSpec
-import androidx.compose.animation.core.animateDecay
-import androidx.compose.animation.core.animateTo
-import androidx.compose.animation.core.calculateTargetValue
-import androidx.compose.animation.core.spring
-import androidx.compose.animation.rememberSplineBasedDecay
-import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.foundation.lazy.LazyListItemInfo
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import kotlin.math.abs
-
-/** Default values used for [SnappingFlingBehavior] & [rememberSnappingFlingBehavior]. */
-internal object SnappingFlingBehaviorDefaults {
- /** TODO */
- val snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = 600f)
-}
-
-/**
- * Create and remember a snapping [FlingBehavior] to be used with [LazyListState].
- *
- * @param lazyListState The [LazyListState] to update.
- * @param decayAnimationSpec The decay animation spec to use for decayed flings.
- * @param snapAnimationSpec The animation spec to use when snapping.
- *
- * TODO: move this to a new module and make it public
- */
-@Composable
-internal fun rememberSnappingFlingBehavior(
- lazyListState: LazyListState,
- decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
- snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec,
-): SnappingFlingBehavior =
- remember(lazyListState, decayAnimationSpec, snapAnimationSpec) {
- SnappingFlingBehavior(
- lazyListState = lazyListState,
- decayAnimationSpec = decayAnimationSpec,
- snapAnimationSpec = snapAnimationSpec,
- )
- }
-
-/**
- * A snapping [FlingBehavior] for [LazyListState]. Typically this would be created via
- * [rememberSnappingFlingBehavior].
- *
- * @param lazyListState The [LazyListState] to update.
- * @param decayAnimationSpec The decay animation spec to use for decayed flings.
- * @param snapAnimationSpec The animation spec to use when snapping.
- */
-internal class SnappingFlingBehavior(
- private val lazyListState: LazyListState,
- private val decayAnimationSpec: DecayAnimationSpec<Float>,
- private val snapAnimationSpec: AnimationSpec<Float>,
-) : FlingBehavior {
- /** The target item index for any on-going animations. */
- var animationTarget: Int? by mutableStateOf(null)
- private set
-
- override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
- val itemInfo = currentItemInfo ?: return initialVelocity
-
- // If the decay fling can scroll past the current item, fling with decay
- return if (decayAnimationSpec.canFlingPastCurrentItem(itemInfo, initialVelocity)) {
- performDecayFling(initialVelocity, itemInfo)
- } else {
- // Otherwise we 'spring' to current/next item
- performSpringFling(
- index =
- when {
- // If the velocity is greater than 1 item per second (velocity is px/s),
- // spring
- // in the relevant direction
- initialVelocity > itemInfo.size -> {
- (itemInfo.index + 1).coerceAtMost(
- lazyListState.layoutInfo.totalItemsCount - 1
- )
- }
- initialVelocity < -itemInfo.size -> itemInfo.index
- // If the velocity is 0 (or less than the size of the item), spring to
- // whichever item is closest to the snap point
- itemInfo.offset < -itemInfo.size / 2 -> itemInfo.index + 1
- else -> itemInfo.index
- },
- initialVelocity = initialVelocity,
- )
- }
- }
-
- private suspend fun ScrollScope.performDecayFling(
- initialVelocity: Float,
- startItem: LazyListItemInfo,
- ): Float {
- val index =
- when {
- initialVelocity > 0 -> startItem.index + 1
- else -> startItem.index
- }
- val forward = index > (currentItemInfo?.index ?: return initialVelocity)
-
- // Update the animationTarget
- animationTarget = index
-
- var velocityLeft = initialVelocity
- var lastValue = 0f
- AnimationState(
- initialValue = 0f,
- initialVelocity = initialVelocity,
- )
- .animateDecay(decayAnimationSpec) {
- val delta = value - lastValue
- val consumed = scrollBy(delta)
- lastValue = value
- velocityLeft = this.velocity
-
- val current = currentItemInfo
- if (current == null) {
- cancelAnimation()
- return@animateDecay
- }
-
- if (
- !forward &&
- (current.index < index || current.index == index && current.offset >= 0)
- ) {
- // 'snap back' to the item as we may have scrolled past it
- scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat())
- cancelAnimation()
- } else if (
- forward &&
- (current.index > index || current.index == index && current.offset <= 0)
- ) {
- // 'snap back' to the item as we may have scrolled past it
- scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat())
- cancelAnimation()
- } else if (abs(delta - consumed) > 0.5f) {
- // avoid rounding errors and stop if anything is unconsumed
- cancelAnimation()
- }
- }
- animationTarget = null
- return velocityLeft
- }
-
- private suspend fun ScrollScope.performSpringFling(
- index: Int,
- scrollOffset: Int = 0,
- initialVelocity: Float = 0f,
- ): Float {
- // If we don't have a current layout, we can't snap
- val initialItem = currentItemInfo ?: return initialVelocity
-
- val forward = index > initialItem.index
- // We add 10% on to the size of the current item, to compensate for any item spacing, etc
- val target = (if (forward) initialItem.size else -initialItem.size) * 1.1f
-
- // Update the animationTarget
- animationTarget = index
-
- var velocityLeft = initialVelocity
- var lastValue = 0f
- AnimationState(
- initialValue = 0f,
- initialVelocity = initialVelocity,
- )
- .animateTo(
- targetValue = target,
- animationSpec = snapAnimationSpec,
- ) {
- // Springs can overshoot their target, clamp to the desired range
- val coercedValue =
- if (forward) {
- value.coerceAtMost(target)
- } else {
- value.coerceAtLeast(target)
- }
- val delta = coercedValue - lastValue
- val consumed = scrollBy(delta)
- lastValue = coercedValue
- velocityLeft = this.velocity
-
- val current = currentItemInfo
- if (current == null) {
- cancelAnimation()
- return@animateTo
- }
-
- if (scrolledPastItem(initialVelocity, current, index, scrollOffset)) {
- // If we've scrolled to/past the item, stop the animation. We may also need to
- // 'snap back' to the item as we may have scrolled past it
- scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat())
- cancelAnimation()
- } else if (abs(delta - consumed) > 0.5f) {
- // avoid rounding errors and stop if anything is unconsumed
- cancelAnimation()
- }
- }
- animationTarget = null
- return velocityLeft
- }
-
- private fun LazyListState.calculateScrollOffsetToItem(index: Int): Int {
- return layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }?.offset ?: 0
- }
-
- private val currentItemInfo: LazyListItemInfo?
- get() =
- lazyListState.layoutInfo.visibleItemsInfo
- .asSequence()
- .filter { it.offset <= 0 && it.offset + it.size > 0 }
- .lastOrNull()
-}
-
-private fun scrolledPastItem(
- initialVelocity: Float,
- currentItem: LazyListItemInfo,
- targetIndex: Int,
- targetScrollOffset: Int = 0,
-): Boolean {
- return if (initialVelocity > 0) {
- // forward
- currentItem.index > targetIndex ||
- (currentItem.index == targetIndex && currentItem.offset <= targetScrollOffset)
- } else {
- // backwards
- currentItem.index < targetIndex ||
- (currentItem.index == targetIndex && currentItem.offset >= targetScrollOffset)
- }
-}
-
-private fun DecayAnimationSpec<Float>.canFlingPastCurrentItem(
- currentItem: LazyListItemInfo,
- initialVelocity: Float,
-): Boolean {
- val targetValue =
- calculateTargetValue(
- initialValue = currentItem.offset.toFloat(),
- initialVelocity = initialVelocity,
- )
- return when {
- // forward. We add 10% onto the size to cater for any item spacing
- initialVelocity > 0 -> targetValue <= -(currentItem.size * 1.1f)
- // backwards. We add 10% onto the size to cater for any item spacing
- else -> targetValue >= (currentItem.size * 0.1f)
- }
-}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
index bef0b3df36c2..ec6e5eda264e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
@@ -14,63 +14,40 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalAnimationApi::class, ExperimentalAnimationGraphicsApi::class)
-
package com.android.systemui.bouncer.ui.composable
import android.view.HapticFeedbackConstants
-import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.AnimationVector1D
-import androidx.compose.animation.core.MutableTransitionState
-import androidx.compose.animation.core.Transition
-import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
-import androidx.compose.animation.core.updateTransition
-import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
-import androidx.compose.animation.graphics.res.animatedVectorResource
-import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
-import androidx.compose.animation.graphics.vector.AnimatedImageVector
-import androidx.compose.foundation.Image
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.compose.animation.Easings
@@ -78,7 +55,6 @@ import com.android.compose.grid.VerticalGrid
import com.android.compose.modifiers.thenIf
import com.android.systemui.R
import com.android.systemui.bouncer.ui.viewmodel.ActionButtonAppearance
-import com.android.systemui.bouncer.ui.viewmodel.EnteredKey
import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
@@ -109,147 +85,6 @@ internal fun PinBouncer(
}
@Composable
-private fun PinInputDisplay(viewModel: PinBouncerViewModel) {
- val currentPinEntries: List<EnteredKey> by viewModel.pinEntries.collectAsState()
-
- // visiblePinEntries keeps pins removed from currentPinEntries in the composition until their
- // disappear-animation completed. The list is sorted by the natural ordering of EnteredKey,
- // which is guaranteed to produce the original edit order, since the model only modifies entries
- // at the end.
- val visiblePinEntries = remember { SnapshotStateList<EnteredKey>() }
- currentPinEntries.forEach {
- val index = visiblePinEntries.binarySearch(it)
- if (index < 0) {
- val insertionPoint = -(index + 1)
- visiblePinEntries.add(insertionPoint, it)
- }
- }
-
- Row(
- modifier =
- Modifier.heightIn(min = entryShapeSize)
- // Pins overflowing horizontally should still be shown as scrolling.
- .wrapContentSize(unbounded = true),
- ) {
- visiblePinEntries.forEachIndexed { index, entry ->
- key(entry) {
- val visibility = remember {
- MutableTransitionState<EntryVisibility>(EntryVisibility.Hidden)
- }
- visibility.targetState =
- when {
- currentPinEntries.isEmpty() && visiblePinEntries.size > 1 ->
- EntryVisibility.BulkHidden(index, visiblePinEntries.size)
- currentPinEntries.contains(entry) -> EntryVisibility.Shown
- else -> EntryVisibility.Hidden
- }
-
- val shape = viewModel.pinShapes.getShape(entry.sequenceNumber)
- PinInputEntry(shape, updateTransition(visibility, label = "Pin Entry $entry"))
-
- LaunchedEffect(entry) {
- // Remove entry from visiblePinEntries once the hide transition completed.
- snapshotFlow {
- visibility.currentState == visibility.targetState &&
- visibility.targetState != EntryVisibility.Shown
- }
- .collect { isRemoved ->
- if (isRemoved) {
- visiblePinEntries.remove(entry)
- }
- }
- }
- }
- }
- }
-}
-
-private sealed class EntryVisibility {
- object Shown : EntryVisibility()
-
- object Hidden : EntryVisibility()
-
- /**
- * Same as [Hidden], but applies when multiple entries are hidden simultaneously, without
- * collapsing during the hide.
- */
- data class BulkHidden(val staggerIndex: Int, val totalEntryCount: Int) : EntryVisibility()
-}
-
-@Composable
-private fun PinInputEntry(shapeResourceId: Int, transition: Transition<EntryVisibility>) {
- // spec: http://shortn/_DEhE3Xl2bi
- val dismissStaggerDelayMs = 33
- val dismissDurationMs = 450
- val expansionDurationMs = 250
- val shapeCollapseDurationMs = 200
-
- val animatedEntryWidth by
- transition.animateDp(
- transitionSpec = {
- when (val target = targetState) {
- is EntryVisibility.BulkHidden ->
- // only collapse horizontal space once all entries are removed
- snap(dismissDurationMs + dismissStaggerDelayMs * target.totalEntryCount)
- else -> tween(expansionDurationMs, easing = Easings.Standard)
- }
- },
- label = "entry space"
- ) { state ->
- if (state == EntryVisibility.Shown) entryShapeSize else 0.dp
- }
-
- val animatedShapeSize by
- transition.animateDp(
- transitionSpec = {
- when {
- EntryVisibility.Hidden isTransitioningTo EntryVisibility.Shown -> {
- // The AVD contains the entry transition.
- snap()
- }
- targetState is EntryVisibility.BulkHidden -> {
- val target = targetState as EntryVisibility.BulkHidden
- tween(
- dismissDurationMs,
- delayMillis = target.staggerIndex * dismissStaggerDelayMs,
- easing = Easings.Legacy,
- )
- }
- else -> tween(shapeCollapseDurationMs, easing = Easings.StandardDecelerate)
- }
- },
- label = "shape size"
- ) { state ->
- if (state == EntryVisibility.Shown) entryShapeSize else 0.dp
- }
-
- val dotColor = MaterialTheme.colorScheme.onSurfaceVariant
- Layout(
- content = {
- val image = AnimatedImageVector.animatedVectorResource(shapeResourceId)
- var atEnd by remember { mutableStateOf(false) }
- Image(
- painter = rememberAnimatedVectorPainter(image, atEnd),
- contentDescription = null,
- contentScale = ContentScale.Crop,
- colorFilter = ColorFilter.tint(dotColor),
- )
- LaunchedEffect(Unit) { atEnd = true }
- }
- ) { measurables, _ ->
- val shapeSizePx = animatedShapeSize.roundToPx()
- val placeable = measurables.single().measure(Constraints.fixed(shapeSizePx, shapeSizePx))
-
- layout(animatedEntryWidth.roundToPx(), entryShapeSize.roundToPx()) {
- placeable.place(
- ((animatedEntryWidth - animatedShapeSize) / 2f).roundToPx(),
- ((entryShapeSize - animatedShapeSize) / 2f).roundToPx()
- )
- }
- }
-}
-
-@Composable
private fun PinPad(viewModel: PinBouncerViewModel) {
val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState()
@@ -511,8 +346,6 @@ private suspend fun showFailureAnimation(
}
}
-private val entryShapeSize = 30.dp
-
private val pinButtonSize = 84.dp
private val pinButtonErrorShrinkFactor = 67.dp / pinButtonSize
private const val pinButtonErrorShrinkMs = 50
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
new file mode 100644
index 000000000000..77065cfdeb76
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
@@ -0,0 +1,437 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+@file:OptIn(ExperimentalAnimationGraphicsApi::class)
+
+package com.android.systemui.bouncer.ui.composable
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
+import androidx.compose.animation.graphics.res.animatedVectorResource
+import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
+import androidx.compose.animation.graphics.vector.AnimatedImageVector
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.runtime.toMutableStateList
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.android.compose.animation.Easings
+import com.android.keyguard.PinShapeAdapter
+import com.android.systemui.R
+import com.android.systemui.bouncer.ui.viewmodel.EntryToken.Digit
+import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.PinInputViewModel
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.joinAll
+import kotlinx.coroutines.launch
+
+@Composable
+fun PinInputDisplay(viewModel: PinBouncerViewModel) {
+ val hintedPinLength: Int? by viewModel.hintedPinLength.collectAsState()
+ val shapeAnimations = rememberShapeAnimations(viewModel.pinShapes)
+
+ // The display comes in two different flavors:
+ // 1) hinting: shows a circle (◦) per expected pin input, and dot (●) per entered digit.
+ // This has a fixed width, and uses two distinct types of AVDs to animate the addition and
+ // removal of digits.
+ // 2) regular, shows a dot (●) per entered digit.
+ // This grows/shrinks as digits are added deleted. Uses the same type of AVDs to animate the
+ // addition of digits, but simply center-shrinks the dot (●) shape to zero to animate the
+ // removal.
+ // Because of all these differences, there are two separate implementations, rather than
+ // unifying into a single, more complex implementation.
+
+ when (val length = hintedPinLength) {
+ null -> RegularPinInputDisplay(viewModel, shapeAnimations)
+ else -> HintingPinInputDisplay(viewModel, shapeAnimations, length)
+ }
+}
+
+/**
+ * A pin input display that shows a placeholder circle (◦) for every digit in the pin not yet
+ * entered.
+ *
+ * Used for auto-confirmed pins of a specific length, see design: http://shortn/_jS8kPzQ7QV
+ */
+@Composable
+private fun HintingPinInputDisplay(
+ viewModel: PinBouncerViewModel,
+ shapeAnimations: ShapeAnimations,
+ hintedPinLength: Int,
+) {
+ val pinInput: PinInputViewModel by viewModel.pinInput.collectAsState()
+ // [ClearAll] marker pointing at the beginning of the current pin input.
+ // When a new [ClearAll] token is added to the [pinInput], the clear-all animation is played
+ // and the marker is advanced manually to the most recent marker. See LaunchedEffect below.
+ var currentClearAll by remember { mutableStateOf(pinInput.mostRecentClearAll()) }
+ // The length of the pin currently entered by the user.
+ val currentPinLength = pinInput.getDigits(currentClearAll).size
+
+ // The animated vector drawables for each of the [hintedPinLength] slots.
+ // The first [currentPinLength] drawables end in a dot (●) shape, the remaining drawables up to
+ // [hintedPinLength] end in the circle (◦) shape.
+ // This list is re-generated upon each pin entry, it is modelled as a [MutableStateList] to
+ // allow the clear-all animation to replace the shapes asynchronously, see LaunchedEffect below.
+ // Note that when a [ClearAll] token is added to the input (and the clear-all animation plays)
+ // the [currentPinLength] does not change; the [pinEntryDrawable] is remembered until the
+ // clear-all animation finishes and the [currentClearAll] state is manually advanced.
+ val pinEntryDrawable =
+ remember(currentPinLength) {
+ buildList {
+ repeat(currentPinLength) { add(shapeAnimations.getShapeToDot(it)) }
+ repeat(hintedPinLength - currentPinLength) { add(shapeAnimations.dotToCircle) }
+ }
+ .toMutableStateList()
+ }
+
+ val mostRecentClearAll = pinInput.mostRecentClearAll()
+ // Whenever a new [ClearAll] marker is added to the input, the clear-all animation needs to
+ // be played.
+ LaunchedEffect(mostRecentClearAll) {
+ if (currentClearAll == mostRecentClearAll) {
+ // Except during the initial composition.
+ return@LaunchedEffect
+ }
+
+ // Staggered replace of all dot (●) shapes with an animation from dot (●) to circle (◦).
+ for (index in 0 until hintedPinLength) {
+ if (!shapeAnimations.isDotShape(pinEntryDrawable[index])) break
+
+ pinEntryDrawable[index] = shapeAnimations.dotToCircle
+ delay(shapeAnimations.dismissStaggerDelay)
+ }
+
+ // Once the animation is done, start processing the next pin input again.
+ currentClearAll = mostRecentClearAll
+ }
+
+ // During the initial composition, do not play the [pinEntryDrawable] animations. This prevents
+ // the dot (●) to circle (◦) animation when the empty display becomes first visible, and a
+ // superfluous shape to dot (●) animation after for example device rotation.
+ var playAnimation by remember { mutableStateOf(false) }
+ LaunchedEffect(Unit) { playAnimation = true }
+
+ val dotColor = MaterialTheme.colorScheme.onSurfaceVariant
+ Row(modifier = Modifier.heightIn(min = shapeAnimations.shapeSize)) {
+ pinEntryDrawable.forEachIndexed { index, drawable ->
+ // Key the loop by [index] and [drawable], so that updating a shape drawable at the same
+ // index will play the new animation (by remembering a new [atEnd]).
+ key(index, drawable) {
+ // [rememberAnimatedVectorPainter] requires a `atEnd` boolean to switch from `false`
+ // to `true` for the animation to play. This animation is suppressed when
+ // playAnimation is false, always rendering the end-state of the animation.
+ var atEnd by remember { mutableStateOf(!playAnimation) }
+ LaunchedEffect(Unit) { atEnd = true }
+
+ Image(
+ painter = rememberAnimatedVectorPainter(drawable, atEnd),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ colorFilter = ColorFilter.tint(dotColor),
+ )
+ }
+ }
+ }
+}
+
+/**
+ * A pin input that shows a dot (●) for each entered pin, horizontally centered and growing /
+ * shrinking as more digits are entered and deleted.
+ *
+ * Used for pin input when the pin length is not hinted, see design http://shortn/_wNP7SrBD78
+ */
+@Composable
+private fun RegularPinInputDisplay(
+ viewModel: PinBouncerViewModel,
+ shapeAnimations: ShapeAnimations,
+) {
+ // Holds all currently [VisiblePinEntry] composables. This cannot be simply derived from
+ // `viewModel.pinInput` at composition, since deleting a pin entry needs to play a remove
+ // animation, thus the composable to be removed has to remain in the composition until fully
+ // disappeared (see `prune` launched effect below)
+ val pinInputRow = remember(shapeAnimations) { PinInputRow(shapeAnimations) }
+
+ // Processed `viewModel.pinInput` updates and applies them to [pinDigitShapes]
+ LaunchedEffect(viewModel.pinInput, pinInputRow) {
+ // Initial setup: capture the most recent [ClearAll] marker and create the visuals for the
+ // existing digits (if any) without animation..
+ var currentClearAll =
+ with(viewModel.pinInput.value) {
+ val initialClearAll = mostRecentClearAll()
+ pinInputRow.setDigits(getDigits(initialClearAll))
+ initialClearAll
+ }
+
+ viewModel.pinInput.collect { input ->
+ // Process additions and removals of pins within the current input block.
+ pinInputRow.updateDigits(input.getDigits(currentClearAll), scope = this@LaunchedEffect)
+
+ val mostRecentClearAll = input.mostRecentClearAll()
+ if (currentClearAll != mostRecentClearAll) {
+ // A new [ClearAll] token is added to the [input], play the clear-all animation
+ pinInputRow.playClearAllAnimation()
+
+ // Animation finished, advance manually to the next marker.
+ currentClearAll = mostRecentClearAll
+ }
+ }
+ }
+
+ LaunchedEffect(pinInputRow) {
+ // Prunes unused VisiblePinEntries once they are no longer visible.
+ snapshotFlow { pinInputRow.hasUnusedEntries() }
+ .collect { hasUnusedEntries ->
+ if (hasUnusedEntries) {
+ pinInputRow.prune()
+ }
+ }
+ }
+
+ pinInputRow.Content()
+}
+
+private class PinInputRow(
+ val shapeAnimations: ShapeAnimations,
+) {
+ private val entries = mutableStateListOf<PinInputEntry>()
+
+ @Composable
+ fun Content() {
+ Row(
+ modifier =
+ Modifier.heightIn(min = shapeAnimations.shapeSize)
+ // Pins overflowing horizontally should still be shown as scrolling.
+ .wrapContentSize(unbounded = true),
+ ) {
+ entries.forEach { entry -> key(entry.digit) { entry.Content() } }
+ }
+ }
+
+ /**
+ * Replaces all current [PinInputEntry] composables with new instances for each digit.
+ *
+ * Does not play the entry expansion animation.
+ */
+ fun setDigits(digits: List<Digit>) {
+ entries.clear()
+ entries.addAll(digits.map { PinInputEntry(it, shapeAnimations) })
+ }
+
+ /**
+ * Adds [PinInputEntry] composables for new digits and plays an entry animation, and starts the
+ * exit animation for digits not in [updated] anymore.
+ *
+ * The function return immediately, playing the animations in the background.
+ *
+ * Removed entries have to be [prune]d once the exit animation completes, [hasUnusedEntries] can
+ * be used in a [SnapshotFlow] to discover when its time to do so.
+ */
+ fun updateDigits(updated: List<Digit>, scope: CoroutineScope) {
+ val incoming = updated.minus(entries.map { it.digit }.toSet()).toList()
+ val outgoing = entries.filterNot { entry -> updated.any { entry.digit == it } }.toList()
+
+ entries.addAll(
+ incoming.map {
+ PinInputEntry(it, shapeAnimations).apply { scope.launch { animateAppearance() } }
+ }
+ )
+
+ outgoing.forEach { entry -> scope.launch { entry.animateRemoval() } }
+
+ entries.sortWith(compareBy { it.digit })
+ }
+
+ /**
+ * Plays a staggered remove animation, and upon completion removes the [PinInputEntry]
+ * composables.
+ *
+ * This function returns once the animation finished playing and the entries are removed.
+ */
+ suspend fun playClearAllAnimation() = coroutineScope {
+ val entriesToRemove = entries.toList()
+ entriesToRemove
+ .mapIndexed { index, entry ->
+ launch {
+ delay(shapeAnimations.dismissStaggerDelay * index)
+ entry.animateClearAllCollapse()
+ }
+ }
+ .joinAll()
+
+ // Remove all [PinInputEntry] composables for which the staggered remove animation was
+ // played. Note that up to now, each PinInputEntry still occupied the full width.
+ entries.removeAll(entriesToRemove)
+ }
+
+ /**
+ * Whether there are [PinInputEntry] that can be removed from the composition since they were
+ * fully animated out.
+ */
+ fun hasUnusedEntries(): Boolean {
+ return entries.any { it.isUnused }
+ }
+
+ /** Remove all no longer visible [PinInputEntry]s from the composition. */
+ fun prune() {
+ entries.removeAll { it.isUnused }
+ }
+}
+
+private class PinInputEntry(
+ val digit: Digit,
+ val shapeAnimations: ShapeAnimations,
+) {
+ private val shape = shapeAnimations.getShapeToDot(digit.sequenceNumber)
+ // horizontal space occupied, used to shift contents as individual digits are animated in/out
+ private val entryWidth =
+ Animatable(shapeAnimations.shapeSize, Dp.VectorConverter, label = "Width of pin ($digit)")
+ // intrinsic width and height of the shape, used to collapse the shape during exit animations.
+ private val shapeSize =
+ Animatable(shapeAnimations.shapeSize, Dp.VectorConverter, label = "Size of pin ($digit)")
+
+ /**
+ * Whether the is fully animated out. When `true`, removing this from the composable won't have
+ * visual effects.
+ */
+ val isUnused: Boolean
+ get() {
+ return entryWidth.targetValue == 0.dp && !entryWidth.isRunning
+ }
+
+ /** Animate the shape appearance by growing the entry width from 0.dp to the intrinsic width. */
+ suspend fun animateAppearance() = coroutineScope {
+ entryWidth.snapTo(0.dp)
+ entryWidth.animateTo(shapeAnimations.shapeSize, shapeAnimations.inputShiftAnimationSpec)
+ }
+
+ /**
+ * Animates shape disappearance by collapsing the shape and occupied horizontal space.
+ *
+ * Once complete, [isUnused] will return `true`.
+ */
+ suspend fun animateRemoval() = coroutineScope {
+ awaitAll(
+ async { entryWidth.animateTo(0.dp, shapeAnimations.inputShiftAnimationSpec) },
+ async { shapeSize.animateTo(0.dp, shapeAnimations.deleteShapeSizeAnimationSpec) }
+ )
+ }
+
+ /** Collapses the shape in place, while still holding on to the horizontal space. */
+ suspend fun animateClearAllCollapse() = coroutineScope {
+ shapeSize.animateTo(0.dp, shapeAnimations.clearAllShapeSizeAnimationSpec)
+ }
+
+ @Composable
+ fun Content() {
+ val animatedShapeSize by shapeSize.asState()
+ val animatedEntryWidth by entryWidth.asState()
+
+ val dotColor = MaterialTheme.colorScheme.onSurfaceVariant
+ val shapeHeight = shapeAnimations.shapeSize
+ var atEnd by remember { mutableStateOf(false) }
+ LaunchedEffect(Unit) { atEnd = true }
+ Image(
+ painter = rememberAnimatedVectorPainter(shape, atEnd),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ colorFilter = ColorFilter.tint(dotColor),
+ modifier =
+ Modifier.layout { measurable, _ ->
+ val shapeSizePx = animatedShapeSize.roundToPx()
+ val placeable = measurable.measure(Constraints.fixed(shapeSizePx, shapeSizePx))
+
+ layout(animatedEntryWidth.roundToPx(), shapeHeight.roundToPx()) {
+ placeable.place(
+ ((animatedEntryWidth - animatedShapeSize) / 2f).roundToPx(),
+ ((shapeHeight - animatedShapeSize) / 2f).roundToPx()
+ )
+ }
+ },
+ )
+ }
+}
+
+/** Animated Vector Drawables used to render the pin input. */
+private class ShapeAnimations(
+ /** Width and height for all the animation images listed here. */
+ val shapeSize: Dp,
+ /** Transitions from the dot (●) to the circle (◦). Used for the hinting pin input only. */
+ val dotToCircle: AnimatedImageVector,
+ /** Each of the animations transition from nothing via a shape to the dot (●). */
+ private val shapesToDot: List<AnimatedImageVector>,
+) {
+ /**
+ * Returns a transition from nothing via shape to the dot (●)., specific to the input position.
+ */
+ fun getShapeToDot(position: Int): AnimatedImageVector {
+ return shapesToDot[position.mod(shapesToDot.size)]
+ }
+
+ /**
+ * Whether the [shapeAnimation] is a image returned by [getShapeToDot], and thus is ending in
+ * the dot (●) shape.
+ *
+ * `false` if the shape's end state is the circle (◦).
+ */
+ fun isDotShape(shapeAnimation: AnimatedImageVector): Boolean {
+ return shapeAnimation != dotToCircle
+ }
+
+ // spec: http://shortn/_DEhE3Xl2bi
+ val dismissStaggerDelay = 33.milliseconds
+ val inputShiftAnimationSpec = tween<Dp>(durationMillis = 250, easing = Easings.Standard)
+ val deleteShapeSizeAnimationSpec =
+ tween<Dp>(durationMillis = 200, easing = Easings.StandardDecelerate)
+ val clearAllShapeSizeAnimationSpec = tween<Dp>(durationMillis = 450, easing = Easings.Legacy)
+}
+
+@Composable
+private fun rememberShapeAnimations(pinShapes: PinShapeAdapter): ShapeAnimations {
+ // NOTE: `animatedVectorResource` does remember the returned AnimatedImageVector.
+ val dotToCircle = AnimatedImageVector.animatedVectorResource(R.drawable.pin_dot_delete_avd)
+ val shapesToDot = pinShapes.shapes.map { AnimatedImageVector.animatedVectorResource(it) }
+ val shapeSize = dimensionResource(R.dimen.password_shape_size)
+
+ return remember(dotToCircle, shapesToDot, shapeSize) {
+ ShapeAnimations(shapeSize, dotToCircle, shapesToDot)
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index 5e0761063af2..32986649388d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -92,6 +92,14 @@ private fun Scene(
onSceneChanged: (SceneModel) -> Unit,
modifier: Modifier = Modifier,
) {
+ val destinationScenes: Map<UserAction, SceneModel> by
+ scene.destinationScenes(containerName).collectAsState()
+ val swipeLeftDestinationScene = destinationScenes[UserAction.Swipe(Direction.LEFT)]
+ val swipeUpDestinationScene = destinationScenes[UserAction.Swipe(Direction.UP)]
+ val swipeRightDestinationScene = destinationScenes[UserAction.Swipe(Direction.RIGHT)]
+ val swipeDownDestinationScene = destinationScenes[UserAction.Swipe(Direction.DOWN)]
+ val backDestinationScene = destinationScenes[UserAction.Back]
+
// TODO(b/280880714): replace with the real UI and make sure to call onTransitionProgress.
Box(modifier) {
Column(
@@ -103,14 +111,6 @@ private fun Scene(
modifier = Modifier,
)
- val destinationScenes: Map<UserAction, SceneModel> by
- scene.destinationScenes(containerName).collectAsState()
- val swipeLeftDestinationScene = destinationScenes[UserAction.Swipe(Direction.LEFT)]
- val swipeUpDestinationScene = destinationScenes[UserAction.Swipe(Direction.UP)]
- val swipeRightDestinationScene = destinationScenes[UserAction.Swipe(Direction.RIGHT)]
- val swipeDownDestinationScene = destinationScenes[UserAction.Swipe(Direction.DOWN)]
- val backDestinationScene = destinationScenes[UserAction.Back]
-
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
index 95a9ce960dcd..d43276c00f87 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
@@ -187,6 +187,9 @@ object CustomizationProviderContract {
/** Flag denoting transit clock are enabled in wallpaper picker. */
const val FLAG_NAME_TRANSIT_CLOCK = "lockscreen_custom_transit_clock"
+ /** Flag denoting transit clock are enabled in wallpaper picker. */
+ const val FLAG_NAME_PAGE_TRANSITIONS = "wallpaper_picker_page_transitions"
+
object Columns {
/** String. Unique ID for the flag. */
const val NAME = "name"
diff --git a/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_down.xml b/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_down.xml
new file mode 100644
index 000000000000..16076b17a6e5
--- /dev/null
+++ b/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_down.xml
@@ -0,0 +1,31 @@
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<!-- TODO(b/273761935): This drawable night variant is identical to the standard drawable. Delete once the drawable cache correctly invalidates for attributes that reference colors that change when the UI mode changes. -->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M0,12C0,5.373 5.373,0 12,0C18.627,0 24,5.373 24,12C24,18.627 18.627,24 12,24C5.373,24 0,18.627 0,12Z"
+ android:fillColor="?androidprv:attr/materialColorSurfaceContainerHigh"/>
+ <path
+ android:pathData="M7.607,9.059L6.667,9.999L12,15.332L17.333,9.999L16.393,9.059L12,13.445"
+ android:fillColor="?androidprv:attr/materialColorOnSurface"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_up.xml b/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_up.xml
new file mode 100644
index 000000000000..309770ddd76d
--- /dev/null
+++ b/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_up.xml
@@ -0,0 +1,31 @@
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<!-- TODO(b/273761935): This drawable night variant is identical to the standard drawable. Delete once the drawable cache correctly invalidates for attributes that reference colors that change when the UI mode changes. -->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M0,12C0,5.3726 5.3726,0 12,0C18.6274,0 24,5.3726 24,12C24,18.6274 18.6274,24 12,24C5.3726,24 0,18.6274 0,12Z"
+ android:fillColor="?androidprv:attr/materialColorSurfaceContainerHigh"/>
+ <path
+ android:pathData="M16.3934,14.9393L17.3334,13.9993L12.0001,8.666L6.6667,13.9993L7.6068,14.9393L12.0001,10.5527"
+ android:fillColor="?androidprv:attr/materialColorOnSurface"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml b/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml
index 54bdf18e3076..bc1775ee64ae 100644
--- a/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml
+++ b/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2023 The Android Open Source Project
~
@@ -13,30 +14,22 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="56dp"
- android:height="24dp"
- android:viewportWidth="56"
- android:viewportHeight="24">
- <path
- android:pathData="M12,0L44,0A12,12 0,0 1,56 12L56,12A12,12 0,0 1,44 24L12,24A12,12 0,0 1,0 12L0,12A12,12 0,0 1,12 0z"
- android:fillColor="#ffffff"/>
- <group
- android:scaleX="0.8"
- android:scaleY="0.8"
- android:translateY="2"
- android:translateX="18">
- <path
- android:pathData="M21.5,9C22.3284,9 23,8.3284 23,7.5C23,6.6716 22.3284,6 21.5,6C20.6716,6 20,6.6716 20,7.5C20,8.3284 20.6716,9 21.5,9Z"
- android:fillColor="#000000"/>
- <path
- android:pathData="M17,14C18.6569,14 20,12.6569 20,11C20,9.3432 18.6569,8 17,8C15.3431,8 14,9.3432 14,11C14,12.6569 15.3431,14 17,14Z"
- android:fillColor="#000000"/>
- <path
- android:pathData="M17,22C18.933,22 20.5,20.433 20.5,18.5C20.5,16.567 18.933,15 17,15C15.067,15 13.5,16.567 13.5,18.5C13.5,20.433 15.067,22 17,22Z"
- android:fillColor="#000000"/>
- <path
- android:pathData="M7,14C10.3137,14 13,11.3137 13,8C13,4.6863 10.3137,2 7,2C3.6863,2 1,4.6863 1,8C1,11.3137 3.6863,14 7,14Z"
- android:fillColor="#000000"/>
- </group>
-</vector> \ No newline at end of file
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@id/background"
+ android:gravity="center">
+ <shape android:shape="oval">
+ <size
+ android:height="24px"
+ android:width="24px"
+ />
+ <solid android:color="#FFFFFFFF" />
+ </shape>
+ </item>
+ <item android:id="@id/icon"
+ android:gravity="center"
+ android:width="20px"
+ android:height="20px"
+ android:drawable="@drawable/ic_person_outline"
+ />
+</layer-list> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_person_outline.xml b/packages/SystemUI/res/drawable/ic_person_outline.xml
new file mode 100644
index 000000000000..d94714e0d51a
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_person_outline.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/black"
+ android:pathData="M480,480Q414,480 367,433Q320,386 320,320Q320,254 367,207Q414,160 480,160Q546,160 593,207Q640,254 640,320Q640,386 593,433Q546,480 480,480ZM160,800L160,688Q160,654 177.5,625.5Q195,597 224,582Q286,551 350,535.5Q414,520 480,520Q546,520 610,535.5Q674,551 736,582Q765,597 782.5,625.5Q800,654 800,688L800,800L160,800ZM240,720L720,720L720,688Q720,677 714.5,668Q709,659 700,654Q646,627 591,613.5Q536,600 480,600Q424,600 369,613.5Q314,627 260,654Q251,659 245.5,668Q240,677 240,688L240,720ZM480,400Q513,400 536.5,376.5Q560,353 560,320Q560,287 536.5,263.5Q513,240 480,240Q447,240 423.5,263.5Q400,287 400,320Q400,353 423.5,376.5Q447,400 480,400ZM480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320ZM480,720L480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720L480,720L480,720Z"/>
+</vector> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/immersive_cling_bg_circ.xml b/packages/SystemUI/res/drawable/immersive_cling_bg_circ.xml
index 4029702ec6b4..32e88ab22b91 100644
--- a/packages/SystemUI/res/drawable/immersive_cling_bg_circ.xml
+++ b/packages/SystemUI/res/drawable/immersive_cling_bg_circ.xml
@@ -17,7 +17,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval" >
- <solid android:color="@android:color/white" />
+ <solid android:color="?android:attr/colorBackground" />
<size
android:height="56dp"
diff --git a/packages/SystemUI/res/drawable/immersive_cling_light_bg_circ.xml b/packages/SystemUI/res/drawable/immersive_cling_light_bg_circ.xml
index e3c7d0ce89aa..12c3e23bf0a0 100644
--- a/packages/SystemUI/res/drawable/immersive_cling_light_bg_circ.xml
+++ b/packages/SystemUI/res/drawable/immersive_cling_light_bg_circ.xml
@@ -17,7 +17,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval" >
- <solid android:color="#80ffffff" />
+ <solid android:color="?android:attr/colorBackground" />
<size
android:height="76dp"
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_background_circle.xml b/packages/SystemUI/res/drawable/privacy_dialog_background_circle.xml
new file mode 100644
index 000000000000..f63c2ffbfdad
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_background_circle.xml
@@ -0,0 +1,29 @@
+<!--
+ ~ 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.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:colorControlHighlight">
+ <item android:id="@android:id/background">
+ <shape
+ android:shape="oval"
+ android:id="@id/background"
+ android:gravity="center">
+ <size
+ android:height="24dp"
+ android:width="24dp"/>
+ <solid android:color="@android:color/white"/>
+ </shape>
+ </item>
+</ripple> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_large_bottom.xml b/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_large_bottom.xml
new file mode 100644
index 000000000000..5d5529ff1a50
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_large_bottom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_large" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_large" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+</ripple>
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_small_bottom.xml b/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_small_bottom.xml
new file mode 100644
index 000000000000..310b0becf10c
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_small_bottom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_large" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_large" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+</ripple>
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_large_bottom.xml b/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_large_bottom.xml
new file mode 100644
index 000000000000..e89bdd31ca17
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_large_bottom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_small" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_small" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+</ripple>
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_small_bottom.xml b/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_small_bottom.xml
new file mode 100644
index 000000000000..fcf0b1c5091f
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_small_bottom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_small" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_small" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+</ripple>
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_check_icon.xml b/packages/SystemUI/res/drawable/privacy_dialog_check_icon.xml
new file mode 100644
index 000000000000..b9f5d60abd8b
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_check_icon.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M9.55,18l-5.7,-5.7 1.425,-1.425L9.55,15.15l9.175,-9.175L20.15,7.4z"/>
+</vector> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_default_permission_icon.xml b/packages/SystemUI/res/drawable/privacy_dialog_default_permission_icon.xml
new file mode 100644
index 000000000000..ea8f9c2839ec
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_default_permission_icon.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24.0dp"
+ android:height="24.0dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M26.0,14.0l-4.0,0.0l0.0,4.0l4.0,0.0l0.0,-4.0zm0.0,8.0l-4.0,0.0l0.0,12.0l4.0,0.0L26.0,22.0zm8.0,-19.98L14.0,2.0c-2.21,0.0 -4.0,1.79 -4.0,4.0l0.0,36.0c0.0,2.21 1.79,4.0 4.0,4.0l20.0,0.0c2.21,0.0 4.0,-1.79 4.0,-4.0L38.0,6.0c0.0,-2.21 -1.79,-3.98 -4.0,-3.98zM34.0,38.0L14.0,38.0L14.0,10.0l20.0,0.0l0.0,28.0z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_down.xml b/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_down.xml
new file mode 100644
index 000000000000..f8b99f4a0ee4
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_down.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M0,12C0,5.373 5.373,0 12,0C18.627,0 24,5.373 24,12C24,18.627 18.627,24 12,24C5.373,24 0,18.627 0,12Z"
+ android:fillColor="?androidprv:attr/materialColorSurfaceContainerHigh"/>
+ <path
+ android:pathData="M7.607,9.059L6.667,9.999L12,15.332L17.333,9.999L16.393,9.059L12,13.445"
+ android:fillColor="?androidprv:attr/materialColorOnSurface"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_up.xml b/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_up.xml
new file mode 100644
index 000000000000..ae60d517ceb4
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_up.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M0,12C0,5.3726 5.3726,0 12,0C18.6274,0 24,5.3726 24,12C24,18.6274 18.6274,24 12,24C5.3726,24 0,18.6274 0,12Z"
+ android:fillColor="?androidprv:attr/materialColorSurfaceContainerHigh"/>
+ <path
+ android:pathData="M16.3934,14.9393L17.3334,13.9993L12.0001,8.666L6.6667,13.9993L7.6068,14.9393L12.0001,10.5527"
+ android:fillColor="?androidprv:attr/materialColorOnSurface"/>
+</vector>
diff --git a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
index bb32022a0b5f..82410703c9e6 100644
--- a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
@@ -110,7 +110,7 @@
<ImageView
android:id="@+id/dream_overlay_assistant_attention_indicator"
- android:layout_width="@dimen/dream_overlay_grey_chip_width"
+ android:layout_width="@dimen/dream_overlay_status_bar_icon_size"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/dream_overlay_status_icon_margin"
android:src="@drawable/dream_overlay_assistant_attention_indicator"
diff --git a/packages/SystemUI/res/layout/immersive_mode_cling.xml b/packages/SystemUI/res/layout/immersive_mode_cling.xml
index bfb8184ee044..e6529b9aa9a1 100644
--- a/packages/SystemUI/res/layout/immersive_mode_cling.xml
+++ b/packages/SystemUI/res/layout/immersive_mode_cling.xml
@@ -58,7 +58,7 @@
android:paddingStart="48dp"
android:paddingTop="40dp"
android:text="@string/immersive_cling_title"
- android:textColor="@android:color/white"
+ android:textColor="?android:attr/textColorPrimaryInverse"
android:textSize="24sp" />
<TextView
@@ -70,7 +70,7 @@
android:paddingStart="48dp"
android:paddingTop="12.6dp"
android:text="@string/immersive_cling_description"
- android:textColor="@android:color/white"
+ android:textColor="?android:attr/textColorPrimaryInverse"
android:textSize="16sp" />
<Button
@@ -85,7 +85,7 @@
android:paddingEnd="8dp"
android:paddingStart="8dp"
android:text="@string/immersive_cling_positive"
- android:textColor="@android:color/white"
+ android:textColor="?android:attr/textColorPrimaryInverse"
android:textSize="14sp" />
</RelativeLayout>
diff --git a/packages/SystemUI/res/layout/privacy_dialog_card_button.xml b/packages/SystemUI/res/layout/privacy_dialog_card_button.xml
new file mode 100644
index 000000000000..e297b939e2b8
--- /dev/null
+++ b/packages/SystemUI/res/layout/privacy_dialog_card_button.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+<Button
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="56dp"
+ android:layout_marginBottom="4dp"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:clickable="true"
+ android:focusable="true"
+ android:gravity="center"
+ style="@style/Widget.Dialog.Button.BorderButton"/> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/privacy_dialog_item_v2.xml b/packages/SystemUI/res/layout/privacy_dialog_item_v2.xml
new file mode 100644
index 000000000000..b84f3a9794be
--- /dev/null
+++ b/packages/SystemUI/res/layout/privacy_dialog_item_v2.xml
@@ -0,0 +1,89 @@
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+<androidx.cardview.widget.CardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/privacy_dialog_item_card"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
+ android:foreground="?android:attr/selectableItemBackground"
+ app:cardCornerRadius="28dp"
+ app:cardElevation="0dp"
+ app:cardBackgroundColor="?androidprv:attr/materialColorSurfaceBright">
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <LinearLayout
+ android:id="@+id/privacy_dialog_item_header"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="20dp"
+ android:paddingBottom="20dp"
+ android:paddingStart="24dp"
+ android:paddingEnd="24dp">
+ <ImageView
+ android:id="@+id/privacy_dialog_item_header_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_centerVertical="true"
+ android:importantForAccessibility="no" />
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="match_parent"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ android:layout_centerVertical="true">
+ <TextView
+ android:id="@+id/privacy_dialog_item_header_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="2dp"
+ android:hyphenationFrequency="normalFast"
+ android:textAlignment="viewStart"
+ android:textAppearance="@style/TextAppearance.PrivacyDialog.Item.Title" />
+ <TextView
+ android:id="@+id/privacy_dialog_item_header_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="viewStart"
+ android:textAppearance="@style/TextAppearance.PrivacyDialog.Item.Summary" />
+ </LinearLayout>
+ <ImageView
+ android:id="@+id/privacy_dialog_item_header_expand_toggle"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_centerVertical="true"
+ android:visibility="gone" />
+ </LinearLayout>
+ <LinearLayout
+ android:id="@+id/privacy_dialog_item_header_expanded_layout"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="16dp"
+ android:paddingStart="24dp"
+ android:paddingEnd="24dp"
+ android:visibility="gone">
+ </LinearLayout>
+ </LinearLayout>
+</androidx.cardview.widget.CardView> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/privacy_dialog_v2.xml b/packages/SystemUI/res/layout/privacy_dialog_v2.xml
new file mode 100644
index 000000000000..843dad03bca4
--- /dev/null
+++ b/packages/SystemUI/res/layout/privacy_dialog_v2.xml
@@ -0,0 +1,109 @@
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+<androidx.core.widget.NestedScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:layout_width="@dimen/large_dialog_width"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="24dp"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ android:orientation="vertical">
+
+ <!-- Header -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:gravity="center">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:fontFamily="@*android:string/config_headlineFontFamily"
+ android:text="@string/privacy_dialog_title"
+ android:layout_marginBottom="12dp"/>
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/privacy_dialog_summary"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?androidprv:attr/materialColorOnSurfaceVariant"
+ android:gravity="center"
+ android:layout_marginBottom="20dp"/>
+ </LinearLayout>
+
+ <!-- Items -->
+ <LinearLayout
+ android:id="@+id/privacy_dialog_items_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="20dp"
+ android:orientation="vertical"
+ />
+
+ <!-- Buttons -->
+ <LinearLayout
+ android:id="@+id/button_layout"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="18dp"
+ android:clickable="false"
+ android:focusable="false">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="start|center_vertical"
+ android:orientation="vertical">
+ <Button
+ android:id="@+id/privacy_dialog_more_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/privacy_dialog_more_button"
+ android:ellipsize="end"
+ android:maxLines="1"
+ style="@style/Widget.Dialog.Button.BorderButton"
+ android:clickable="true"
+ android:focusable="true"
+ android:gravity="center"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end|center_vertical">
+ <Button
+ android:id="@+id/privacy_dialog_close_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/privacy_dialog_done_button"
+ android:ellipsize="end"
+ android:maxLines="1"
+ style="@style/Widget.Dialog.Button.BorderButton"
+ android:clickable="true"
+ android:focusable="true"
+ android:gravity="center"/>
+ </LinearLayout>
+ </LinearLayout>
+ </LinearLayout>
+</androidx.core.widget.NestedScrollView> \ No newline at end of file
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index db7eb7a049e7..ab754985e11d 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -16,7 +16,8 @@
* limitations under the License.
*/
-->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
<drawable name="notification_number_text_color">#ffffffff</drawable>
<drawable name="system_bar_background">@color/system_bar_background_opaque</drawable>
<color name="system_bar_background_opaque">#ff000000</color>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 0e88c3178d11..5a15dcec5223 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1097,6 +1097,9 @@
<dimen name="ongoing_appops_dialog_side_padding">16dp</dimen>
+ <dimen name="privacy_dialog_background_radius_large">12dp</dimen>
+ <dimen name="privacy_dialog_background_radius_small">4dp</dimen>
+
<!-- Size of media cards in the QSPanel carousel -->
<dimen name="qs_media_padding">16dp</dimen>
<dimen name="qs_media_album_radius">14dp</dimen>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 3a2177a0045c..15ca9d48c62a 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -214,4 +214,8 @@
<item type="id" name="nssl_guideline" />
<item type="id" name="lock_icon" />
<item type="id" name="lock_icon_bg" />
+
+ <!-- Privacy dialog -->
+ <item type="id" name="privacy_dialog_close_app_button" />
+ <item type="id" name="privacy_dialog_manage_app_button" />
</resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index c306a79b859f..983b09f957ae 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -369,6 +369,8 @@
<string name="biometric_dialog_tap_confirm_with_face_alt_3">Face recognized. Press the unlock icon to continue.</string>
<!-- Talkback string when a biometric is authenticated [CHAR LIMIT=NONE] -->
<string name="biometric_dialog_authenticated">Authenticated</string>
+ <!-- Talkback string when a canceling authentication [CHAR LIMIT=NONE] -->
+ <string name="biometric_dialog_cancel_authentication">Cancel Authentication</string>
<!-- Button text shown on BiometricPrompt giving the user the option to use an alternate form of authentication (Pin) [CHAR LIMIT=30] -->
<string name="biometric_dialog_use_pin">Use PIN</string>
@@ -953,6 +955,8 @@
<!-- Message shown when face authentication fails and the pin pad is visible. [CHAR LIMIT=60] -->
<string name="keyguard_retry">Swipe up to try again</string>
+ <!-- Message shown when face authentication fails and the pin pad is visible. [CHAR LIMIT=60] -->
+ <string name="accesssibility_keyguard_retry">Swipe up to try Face Unlock again</string>
<!-- Message shown when notifying user to unlock in order to use NFC. [CHAR LIMIT=60] -->
<string name="require_unlock_for_nfc">Unlock to use NFC</string>
@@ -3170,10 +3174,43 @@
<!--- Content of toast triggered when the notes app entry point is triggered without setting a default notes app. [CHAR LIMIT=NONE] -->
<string name="set_default_notes_app_toast_content">Set default notes app in Settings</string>
- <!--
- Label for a button that, when clicked, sends the user to the app store to install an app.
-
- [CHAR LIMIT=64].
- -->
+ <!-- Label for a button that, when clicked, sends the user to the app store to install an app. [CHAR LIMIT=64]. -->
<string name="install_app">Install app</string>
+
+ <!-- Title of the privacy dialog, shown for active / recent app usage of some phone sensors [CHAR LIMIT=30] -->
+ <string name="privacy_dialog_title">Microphone &amp; Camera</string>
+ <!-- Subtitle of the privacy dialog, shown for active / recent app usage of some phone sensors [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_summary">Recent app use</string>
+ <!-- Label of the secondary button of the privacy dialog, used to check recent app usage of phone sensors [CHAR LIMIT=30] -->
+ <string name="privacy_dialog_more_button">See recent access</string>
+ <!-- Label of the primary button to dismiss the privacy dialog [CHAR LIMIT=20] -->
+ <string name="privacy_dialog_done_button">Done</string>
+ <!-- Description for expanding a collapsible widget in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_expand_action">Expand and show options</string>
+ <!-- Description for collapsing a collapsible widget in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_collapse_action">Collapse</string>
+ <!-- Label of a button of the privacy dialog to close an app actively using a phone sensor [CHAR LIMIT=50] -->
+ <string name="privacy_dialog_close_app_button">Close this app</string>
+ <!-- Message shown in the privacy dialog when an app actively using a phone sensor is closed [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_close_app_message"><xliff:g id="app_name" example="Gmail">%1$s</xliff:g> closed</string>
+ <!-- Label of a button of the privacy dialog to learn more of a service actively or recently using a phone sensor [CHAR LIMIT=50] -->
+ <string name="privacy_dialog_manage_service">Manage service</string>
+ <!-- Label of a button of the privacy dialog to manage permissions of an app actively or recently using a phone sensor [CHAR LIMIT=50] -->
+ <string name="privacy_dialog_manage_permissions">Manage access</string>
+ <!-- Label for active usage of a phone sensor by phone call in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_active_call_usage">In use by phone call</string>
+ <!-- Label for recent usage of a phone sensor by phone call in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_recent_call_usage">Recently used in phone call</string>
+ <!-- Label for active app usage of a phone sensor in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_active_app_usage">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g></string>
+ <!-- Label for recent app usage of a phone sensor in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_recent_app_usage">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g></string>
+ <!-- Label for active app usage of a phone sensor with sub-attribution or proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_active_app_usage_1">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g>)</string>
+ <!-- Label for recent app usage of a phone sensor with sub-attribution or proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_recent_app_usage_1">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g>)</string>
+ <!-- Label for active app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_active_app_usage_2">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string>
+ <!-- Label for recent app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_recent_app_usage_2">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string>
</resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 1c46a91e5d29..d520670ec012 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1439,4 +1439,22 @@
<item name="android:windowEnterAnimation">@anim/long_press_lock_screen_popup_enter</item>
<item name="android:windowExitAnimation">@anim/long_press_lock_screen_popup_exit</item>
</style>
+
+ <style name="TextAppearance.PrivacyDialog.Item.Title"
+ parent="@android:style/TextAppearance.DeviceDefault.Medium">
+ <item name="android:textSize">14sp</item>
+ <item name="android:lineHeight">20sp</item>
+ <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
+ </style>
+
+ <style name="TextAppearance.PrivacyDialog.Item.Summary"
+ parent="@android:style/TextAppearance.DeviceDefault.Small">
+ <item name="android:textSize">14sp</item>
+ <item name="android:lineHeight">20sp</item>
+ <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item>
+ </style>
+
+ <style name="Theme.PrivacyDialog" parent="@style/Theme.SystemUI.Dialog">
+ <item name="android:colorBackground">?androidprv:attr/materialColorSurfaceContainer</item>
+ </style>
</resources>
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 9df56fcce430..58adfa1d882c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -58,6 +58,10 @@ import android.widget.ScrollView;
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
+import androidx.core.view.AccessibilityDelegateCompat;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+
import com.android.app.animation.Interpolators;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.jank.InteractionJankMonitor;
@@ -333,6 +337,20 @@ public class AuthContainerView extends LinearLayout
addView(mFrameLayout);
mBiometricScrollView = mFrameLayout.findViewById(R.id.biometric_scrollview);
mBackgroundView = mFrameLayout.findViewById(R.id.background);
+ ViewCompat.setAccessibilityDelegate(mBackgroundView, new AccessibilityDelegateCompat() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host,
+ AccessibilityNodeInfoCompat info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.addAction(
+ new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ AccessibilityNodeInfoCompat.ACTION_CLICK,
+ mContext.getString(R.string.biometric_dialog_cancel_authentication)
+ )
+ );
+ }
+ });
+
mPanelView = mFrameLayout.findViewById(R.id.panel);
mPanelController = new AuthPanelController(mContext, mPanelView);
mBackgroundExecutor = bgExecutor;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
index a5e846ad61ca..53dc0e3d4846 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
@@ -17,6 +17,8 @@
package com.android.systemui.biometrics.dagger
import com.android.settingslib.udfps.UdfpsUtils
+import com.android.systemui.biometrics.data.repository.FacePropertyRepository
+import com.android.systemui.biometrics.data.repository.FacePropertyRepositoryImpl
import com.android.systemui.biometrics.data.repository.FaceSettingsRepository
import com.android.systemui.biometrics.data.repository.FaceSettingsRepositoryImpl
import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
@@ -53,6 +55,10 @@ interface BiometricsModule {
@Binds
@SysUISingleton
+ fun faceSensors(impl: FacePropertyRepositoryImpl): FacePropertyRepository
+
+ @Binds
+ @SysUISingleton
fun biometricPromptRepository(impl: PromptRepositoryImpl): PromptRepository
@Binds
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt
new file mode 100644
index 000000000000..d2cb84945252
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 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.biometrics.data.repository
+
+import android.hardware.face.FaceManager
+import android.hardware.face.FaceSensorPropertiesInternal
+import android.hardware.face.IFaceAuthenticatorsRegisteredCallback
+import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.biometrics.shared.model.toSensorStrength
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
+
+/** A repository for the global state of Face sensor. */
+interface FacePropertyRepository {
+ /** Face sensor information, null if it is not available. */
+ val sensorInfo: Flow<FaceSensorInfo?>
+}
+
+/** Describes a biometric sensor */
+data class FaceSensorInfo(val id: Int, val strength: SensorStrength)
+
+private const val TAG = "FaceSensorPropertyRepositoryImpl"
+
+@SysUISingleton
+class FacePropertyRepositoryImpl
+@Inject
+constructor(@Application private val applicationScope: CoroutineScope, faceManager: FaceManager?) :
+ FacePropertyRepository {
+
+ private val sensorProps: Flow<List<FaceSensorPropertiesInternal>> =
+ faceManager?.let {
+ ConflatedCallbackFlow.conflatedCallbackFlow {
+ val callback =
+ object : IFaceAuthenticatorsRegisteredCallback.Stub() {
+ override fun onAllAuthenticatorsRegistered(
+ sensors: List<FaceSensorPropertiesInternal>
+ ) {
+ trySendWithFailureLogging(
+ sensors,
+ TAG,
+ "onAllAuthenticatorsRegistered"
+ )
+ }
+ }
+ it.addAuthenticatorsRegisteredCallback(callback)
+ awaitClose {}
+ }
+ .shareIn(applicationScope, SharingStarted.Eagerly)
+ }
+ ?: flowOf(emptyList())
+
+ override val sensorInfo: Flow<FaceSensorInfo?> =
+ sensorProps
+ .map { it.firstOrNull() }
+ .map { it?.let { FaceSensorInfo(it.sensorId, it.sensorStrength.toSensorStrength()) } }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
index efc92ad3b4c8..daff5feb0123 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
@@ -22,6 +22,7 @@ import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.biometrics.shared.model.toSensorStrength
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
@@ -106,7 +107,7 @@ constructor(
private fun setProperties(prop: FingerprintSensorPropertiesInternal) {
_sensorId.value = prop.sensorId
- _strength.value = sensorStrengthIntToObject(prop.sensorStrength)
+ _strength.value = prop.sensorStrength.toSensorStrength()
_sensorType.value = sensorTypeIntToObject(prop.sensorType)
_sensorLocations.value =
prop.allLocations.associateBy { sensorLocationInternal ->
@@ -119,15 +120,6 @@ constructor(
}
}
-private fun sensorStrengthIntToObject(value: Int): SensorStrength {
- return when (value) {
- 0 -> SensorStrength.CONVENIENCE
- 1 -> SensorStrength.WEAK
- 2 -> SensorStrength.STRONG
- else -> throw IllegalArgumentException("Invalid SensorStrength value: $value")
- }
-}
-
private fun sensorTypeIntToObject(value: Int): FingerprintSensorType {
return when (value) {
0 -> FingerprintSensorType.UNKNOWN
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt
index 2982d0be3764..30e865eff8b8 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt
@@ -18,9 +18,18 @@ package com.android.systemui.biometrics.shared.model
import android.hardware.biometrics.SensorProperties
-/** Fingerprint sensor security strength. Represents [SensorProperties.Strength]. */
+/** Sensor security strength. Represents [SensorProperties.Strength]. */
enum class SensorStrength {
CONVENIENCE,
WEAK,
STRONG,
}
+
+/** Convert [this] to corresponding [SensorStrength] */
+fun Int.toSensorStrength(): SensorStrength =
+ when (this) {
+ 0 -> SensorStrength.CONVENIENCE
+ 1 -> SensorStrength.WEAK
+ 2 -> SensorStrength.STRONG
+ else -> throw IllegalArgumentException("Invalid SensorStrength value: $this")
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
index 7ae1443203bb..9bbf1ef04481 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -305,6 +305,10 @@ object BiometricViewBinder {
.collect { onClick ->
iconViewOverlay.setOnClickListener(onClick)
iconView.setOnClickListener(onClick)
+ if (onClick == null) {
+ iconViewOverlay.isClickable = false
+ iconView.isClickable = false
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index 38fb8b968775..844cf024ef71 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -40,9 +40,10 @@ class PinBouncerViewModel(
) {
val pinShapes = PinShapeAdapter(applicationContext)
+ private val mutablePinInput = MutableStateFlow(PinInputViewModel.empty())
- private val mutablePinEntries = MutableStateFlow<List<EnteredKey>>(emptyList())
- val pinEntries: StateFlow<List<EnteredKey>> = mutablePinEntries
+ /** Currently entered pin keys. */
+ val pinInput: StateFlow<PinInputViewModel> = mutablePinInput
/** The length of the PIN for which we should show a hint. */
val hintedPinLength: StateFlow<Int?> = interactor.hintedPinLength
@@ -50,11 +51,11 @@ class PinBouncerViewModel(
/** Appearance of the backspace button. */
val backspaceButtonAppearance: StateFlow<ActionButtonAppearance> =
combine(
- mutablePinEntries,
+ mutablePinInput,
interactor.isAutoConfirmEnabled,
) { mutablePinEntries, isAutoConfirmEnabled ->
computeBackspaceButtonAppearance(
- enteredPin = mutablePinEntries,
+ pinInput = mutablePinEntries,
isAutoConfirmEnabled = isAutoConfirmEnabled,
)
}
@@ -89,26 +90,23 @@ class PinBouncerViewModel(
/** Notifies that the user clicked on a PIN button with the given digit value. */
fun onPinButtonClicked(input: Int) {
- if (mutablePinEntries.value.isEmpty()) {
+ val pinInput = mutablePinInput.value
+ if (pinInput.isEmpty()) {
interactor.clearMessage()
}
- mutablePinEntries.value += EnteredKey(input)
-
+ mutablePinInput.value = pinInput.append(input)
tryAuthenticate(useAutoConfirm = true)
}
/** Notifies that the user clicked the backspace button. */
fun onBackspaceButtonClicked() {
- if (mutablePinEntries.value.isEmpty()) {
- return
- }
- mutablePinEntries.value = mutablePinEntries.value.toMutableList().apply { removeLast() }
+ mutablePinInput.value = mutablePinInput.value.deleteLast()
}
/** Notifies that the user long-pressed the backspace button. */
fun onBackspaceButtonLongPressed() {
- mutablePinEntries.value = emptyList()
+ mutablePinInput.value = mutablePinInput.value.clearAll()
}
/** Notifies that the user clicked the "enter" button. */
@@ -117,7 +115,7 @@ class PinBouncerViewModel(
}
private fun tryAuthenticate(useAutoConfirm: Boolean) {
- val pinCode = mutablePinEntries.value.map { it.input }
+ val pinCode = mutablePinInput.value.getPin()
applicationScope.launch {
val isSuccess = interactor.authenticate(pinCode, useAutoConfirm) ?: return@launch
@@ -126,15 +124,17 @@ class PinBouncerViewModel(
showFailureAnimation()
}
- mutablePinEntries.value = emptyList()
+ // TODO(b/291528545): this should not be cleared on success (at least until the view
+ // is animated away).
+ mutablePinInput.value = mutablePinInput.value.clearAll()
}
}
private fun computeBackspaceButtonAppearance(
- enteredPin: List<EnteredKey>,
+ pinInput: PinInputViewModel,
isAutoConfirmEnabled: Boolean,
): ActionButtonAppearance {
- val isEmpty = enteredPin.isEmpty()
+ val isEmpty = pinInput.isEmpty()
return when {
isAutoConfirmEnabled && isEmpty -> ActionButtonAppearance.Hidden
@@ -153,19 +153,3 @@ enum class ActionButtonAppearance {
/** Button is shown. */
Shown,
}
-
-private var nextSequenceNumber = 1
-
-/**
- * The pin bouncer [input] as digits 0-9, together with a [sequenceNumber] to indicate the ordering.
- *
- * Since the model only allows appending/removing [EnteredKey]s from the end, the [sequenceNumber]
- * is strictly increasing in input order of the pin, but not guaranteed to be monotonic or start at
- * a specific number.
- */
-data class EnteredKey
-internal constructor(val input: Int, val sequenceNumber: Int = nextSequenceNumber++) :
- Comparable<EnteredKey> {
- override fun compareTo(other: EnteredKey): Int =
- compareValuesBy(this, other, EnteredKey::sequenceNumber)
-}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModel.kt
new file mode 100644
index 000000000000..4efc21b41e6a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModel.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2023 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.bouncer.ui.viewmodel
+
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.bouncer.ui.viewmodel.EntryToken.ClearAll
+import com.android.systemui.bouncer.ui.viewmodel.EntryToken.Digit
+
+/**
+ * Immutable pin input state.
+ *
+ * The input is a hybrid of state ([Digit]) and event ([ClearAll]) tokens. The [ClearAll] token can
+ * be interpreted as a watermark, indicating that the current input up to that point is deleted
+ * (after a auth failure or when long-pressing the delete button). Therefore, [Digit]s following a
+ * [ClearAll] make up the next pin input entry. Up to two complete pin inputs are memoized.
+ *
+ * This is required when auto-confirm rejects the input, and the last digit will be animated-in at
+ * the end of the input, concurrently with the staggered clear-all animation starting to play at the
+ * beginning of the input.
+ *
+ * The input is guaranteed to always contain a initial [ClearAll] token as a sentinel, thus clients
+ * can always assume there is a 'ClearAll' watermark available.
+ */
+data class PinInputViewModel
+internal constructor(
+ @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal val input: List<EntryToken>
+) {
+ init {
+ require(input.firstOrNull() is ClearAll) { "input does not begin with a ClearAll token" }
+ require(input.zipWithNext().all { it.first < it.second }) {
+ "EntryTokens are not sorted by their sequenceNumber"
+ }
+ }
+ /**
+ * [PinInputViewModel] with [previousInput] and appended [newToken].
+ *
+ * [previousInput] is trimmed so that the new [PinBouncerViewModel] contains at most two pin
+ * inputs.
+ */
+ private constructor(
+ previousInput: List<EntryToken>,
+ newToken: EntryToken
+ ) : this(
+ buildList {
+ addAll(
+ previousInput.subList(previousInput.indexOfLastClearAllToKeep(), previousInput.size)
+ )
+ add(newToken)
+ }
+ )
+
+ fun append(digit: Int): PinInputViewModel {
+ return PinInputViewModel(input, Digit(digit))
+ }
+
+ /**
+ * Delete last digit.
+ *
+ * This removes the last digit from the input. Returns `this` if the last token is [ClearAll].
+ */
+ fun deleteLast(): PinInputViewModel {
+ if (isEmpty()) return this
+ return PinInputViewModel(input.take(input.size - 1))
+ }
+
+ /**
+ * Appends a [ClearAll] watermark, completing the current pin.
+ *
+ * Returns `this` if the last token is [ClearAll].
+ */
+ fun clearAll(): PinInputViewModel {
+ if (isEmpty()) return this
+ return PinInputViewModel(input, ClearAll())
+ }
+
+ /** Whether the current pin is empty. */
+ fun isEmpty(): Boolean {
+ return input.last() is ClearAll
+ }
+
+ /** The current pin, or an empty list if [isEmpty]. */
+ fun getPin(): List<Int> {
+ return getDigits(mostRecentClearAll()).map { it.input }
+ }
+
+ /**
+ * The digits following the specified [ClearAll] marker, up to the next marker or the end of the
+ * input.
+ *
+ * Returns an empty list if the [ClearAll] is not in the input.
+ */
+ fun getDigits(clearAllMarker: ClearAll): List<Digit> {
+ val startIndex = input.indexOf(clearAllMarker) + 1
+ if (startIndex == 0 || startIndex == input.size) return emptyList()
+
+ return input.subList(startIndex, input.size).takeWhile { it is Digit }.map { it as Digit }
+ }
+
+ /** The most recent [ClearAll] marker. */
+ fun mostRecentClearAll(): ClearAll {
+ return input.last { it is ClearAll } as ClearAll
+ }
+
+ companion object {
+ fun empty() = PinInputViewModel(listOf(ClearAll()))
+ }
+}
+
+/**
+ * Pin bouncer entry token with a [sequenceNumber] to indicate input event ordering.
+ *
+ * Since the model only allows appending/removing [Digit]s from the end, the [sequenceNumber] is
+ * strictly increasing in input order of the pin, but not guaranteed to be monotonic or start at a
+ * specific number.
+ */
+sealed interface EntryToken : Comparable<EntryToken> {
+ val sequenceNumber: Int
+
+ /** The pin bouncer [input] as digits 0-9. */
+ data class Digit
+ internal constructor(val input: Int, override val sequenceNumber: Int = nextSequenceNumber++) :
+ EntryToken {
+ init {
+ check(input in 0..9)
+ }
+ }
+
+ /**
+ * Marker to indicate the input is completely cleared, and subsequent [EntryToken]s mark a new
+ * pin entry.
+ */
+ data class ClearAll
+ internal constructor(override val sequenceNumber: Int = nextSequenceNumber++) : EntryToken
+
+ override fun compareTo(other: EntryToken): Int =
+ compareValuesBy(this, other, EntryToken::sequenceNumber)
+
+ companion object {
+ private var nextSequenceNumber = 1
+ }
+}
+
+/**
+ * Index of the last [ClearAll] token to keep for a new [PinInputViewModel], so that after appending
+ * another [EntryToken], there are at most two pin inputs in the [PinInputViewModel].
+ */
+private fun List<EntryToken>.indexOfLastClearAllToKeep(): Int {
+ require(isNotEmpty() && first() is ClearAll)
+
+ var seenClearAll = 0
+ for (i in size - 1 downTo 0) {
+ if (get(i) is ClearAll) {
+ seenClearAll++
+ if (seenClearAll == 2) {
+ return i
+ }
+ }
+ }
+
+ // The first element is guaranteed to be a ClearAll marker.
+ return 0
+}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java
index e342ac2f320d..566a74ae3e07 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java
@@ -17,6 +17,7 @@
package com.android.systemui.clipboardoverlay;
import android.content.ClipData;
+import android.content.ClipDescription;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -41,10 +42,16 @@ class IntentCreator {
// From the ACTION_SEND docs:
// "If using EXTRA_TEXT, the MIME type should be "text/plain"; otherwise it should be the
// MIME type of the data in EXTRA_STREAM"
- if (clipData.getItemAt(0).getUri() != null) {
- shareIntent.setDataAndType(
- clipData.getItemAt(0).getUri(), clipData.getDescription().getMimeType(0));
- shareIntent.putExtra(Intent.EXTRA_STREAM, clipData.getItemAt(0).getUri());
+ Uri uri = clipData.getItemAt(0).getUri();
+ if (uri != null) {
+ // We don't use setData here because some apps interpret this as "to:".
+ shareIntent.setType(clipData.getDescription().getMimeType(0));
+ // Include URI in ClipData also, so that grantPermission picks it up.
+ shareIntent.setClipData(new ClipData(
+ new ClipDescription(
+ "content", new String[]{clipData.getDescription().getMimeType(0)}),
+ new ClipData.Item(uri)));
+ shareIntent.putExtra(Intent.EXTRA_STREAM, uri);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else {
shareIntent.putExtra(
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index a560accfff68..d9665c5b5047 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -48,6 +48,7 @@ import com.android.systemui.reardisplay.RearDisplayDialogController
import com.android.systemui.recents.Recents
import com.android.systemui.settings.dagger.MultiUserUtilsModule
import com.android.systemui.shortcut.ShortcutKeyDispatcher
+import com.android.systemui.statusbar.ImmersiveModeConfirmation
import com.android.systemui.statusbar.notification.InstantAppNotifier
import com.android.systemui.statusbar.phone.KeyguardLiftController
import com.android.systemui.statusbar.phone.LockscreenWallpaper
@@ -162,6 +163,12 @@ abstract class SystemUICoreStartableModule {
@ClassKey(Recents::class)
abstract fun bindRecents(sysui: Recents): CoreStartable
+ /** Inject into ImmersiveModeConfirmation. */
+ @Binds
+ @IntoMap
+ @ClassKey(ImmersiveModeConfirmation::class)
+ abstract fun bindImmersiveModeConfirmation(sysui: ImmersiveModeConfirmation): CoreStartable
+
/** Inject into RingtonePlayer. */
@Binds
@IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index e6820a317035..66813f90bac4 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -109,9 +109,15 @@ object Flags {
sysPropBooleanFlag(
128,
"persist.sysui.notification.builder_extras_override",
- default = false
+ default = true
)
+ /** Only notify group expansion listeners when a change happens. */
+ // TODO(b/292213543): Tracking Bug
+ @JvmField
+ val NOTIFICATION_GROUP_EXPANSION_CHANGE =
+ unreleasedFlag(292213543, "notification_group_expansion_change", teamfood = false)
+
// 200 - keyguard/lockscreen
// ** Flag retired **
// public static final BooleanFlag KEYGUARD_LAYOUT =
@@ -216,6 +222,12 @@ object Flags {
val LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP =
unreleasedFlag(232, "lock_screen_long_press_directly_opens_wallpaper_picker")
+ /** Whether page transition animations in the wallpaper picker are enabled */
+ // TODO(b/291710220): Tracking bug.
+ @JvmField
+ val WALLPAPER_PICKER_PAGE_TRANSITIONS =
+ unreleasedFlag(291710220, "wallpaper_picker_page_transitions")
+
/** Whether to run the new udfps keyguard refactor code. */
// TODO(b/279440316): Tracking bug.
@JvmField
@@ -698,7 +710,7 @@ object Flags {
// TODO(b/283740863): Tracking Bug
@JvmField
val ENABLE_NEW_PRIVACY_DIALOG =
- unreleasedFlag(283740863, "enable_new_privacy_dialog", teamfood = false)
+ unreleasedFlag(283740863, "enable_new_privacy_dialog", teamfood = true)
// TODO(b/289573946): Tracking Bug
@JvmField val PRECOMPUTED_TEXT = unreleasedFlag(289573946, "precomputed_text")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
index 1978b3d048b7..039460d8fdae 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
@@ -61,28 +61,23 @@ constructor(
override fun start() {
Log.d(LOG_TAG, "Resource trimmer registered.")
- if (
- !(featureFlags.isEnabled(Flags.TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK) ||
- featureFlags.isEnabled(Flags.TRIM_FONT_CACHES_AT_UNLOCK))
- ) {
- return
- }
-
- applicationScope.launch(bgDispatcher) {
- // We need to wait for the AoD transition (and animation) to complete.
- // This means we're waiting for isDreaming (== implies isDoze) and dozeAmount == 1f
- // signal. This is to make sure we don't clear font caches during animation which
- // would jank and leave stale data in memory.
- val isDozingFully =
- keyguardInteractor.dozeAmount.map { it == 1f }.distinctUntilChanged()
- combine(
- keyguardInteractor.wakefulnessModel.map { it.state },
- keyguardInteractor.isDreaming,
- isDozingFully,
- ::Triple
- )
- .distinctUntilChanged()
- .collect { onWakefulnessUpdated(it.first, it.second, it.third) }
+ if (featureFlags.isEnabled(Flags.TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK)) {
+ applicationScope.launch(bgDispatcher) {
+ // We need to wait for the AoD transition (and animation) to complete.
+ // This means we're waiting for isDreaming (== implies isDoze) and dozeAmount == 1f
+ // signal. This is to make sure we don't clear font caches during animation which
+ // would jank and leave stale data in memory.
+ val isDozingFully =
+ keyguardInteractor.dozeAmount.map { it == 1f }.distinctUntilChanged()
+ combine(
+ keyguardInteractor.wakefulnessModel.map { it.state },
+ keyguardInteractor.isDreaming,
+ isDozingFully,
+ ::Triple
+ )
+ .distinctUntilChanged()
+ .collect { onWakefulnessUpdated(it.first, it.second, it.third) }
+ }
}
applicationScope.launch(bgDispatcher) {
@@ -97,17 +92,16 @@ constructor(
@WorkerThread
private fun onKeyguardGone() {
- if (!featureFlags.isEnabled(Flags.TRIM_FONT_CACHES_AT_UNLOCK)) {
- return
- }
-
- if (DEBUG) {
- Log.d(LOG_TAG, "Trimming font caches since keyguard went away.")
- }
// We want to clear temporary caches we've created while rendering and animating
// lockscreen elements, especially clocks.
+ Log.d(LOG_TAG, "Sending TRIM_MEMORY_UI_HIDDEN.")
globalWindowManager.trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
- globalWindowManager.trimCaches(HardwareRenderer.CACHE_TRIM_FONT)
+ if (featureFlags.isEnabled(Flags.TRIM_FONT_CACHES_AT_UNLOCK)) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "Trimming font caches since keyguard went away.")
+ }
+ globalWindowManager.trimCaches(HardwareRenderer.CACHE_TRIM_FONT)
+ }
}
@WorkerThread
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
index bf1e75b09bac..6fd3e21b25a4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
@@ -26,6 +26,8 @@ import com.android.internal.logging.UiEventLogger
import com.android.keyguard.FaceAuthUiEvent
import com.android.systemui.Dumpable
import com.android.systemui.R
+import com.android.systemui.biometrics.data.repository.FacePropertyRepository
+import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
@@ -58,6 +60,7 @@ import java.util.stream.Collectors
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
@@ -68,6 +71,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@@ -120,6 +124,7 @@ interface DeviceEntryFaceAuthRepository {
fun cancel()
}
+@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class DeviceEntryFaceAuthRepositoryImpl
@Inject
@@ -143,7 +148,8 @@ constructor(
@FaceDetectTableLog private val faceDetectLog: TableLogBuffer,
@FaceAuthTableLog private val faceAuthLog: TableLogBuffer,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
- private val featureFlags: FeatureFlags,
+ featureFlags: FeatureFlags,
+ facePropertyRepository: FacePropertyRepository,
dumpManager: DumpManager,
) : DeviceEntryFaceAuthRepository, Dumpable {
private var authCancellationSignal: CancellationSignal? = null
@@ -163,6 +169,13 @@ constructor(
override val detectionStatus: Flow<FaceDetectionStatus>
get() = _detectionStatus.filterNotNull()
+ private val isFaceBiometricsAllowed: Flow<Boolean> =
+ facePropertyRepository.sensorInfo.flatMapLatest {
+ if (it?.strength == SensorStrength.STRONG)
+ biometricSettingsRepository.isStrongBiometricAllowed
+ else biometricSettingsRepository.isNonStrongBiometricAllowed
+ }
+
private val _isLockedOut = MutableStateFlow(false)
override val isLockedOut: StateFlow<Boolean> = _isLockedOut
@@ -274,10 +287,8 @@ constructor(
canFaceAuthOrDetectRun(faceDetectLog),
logAndObserve(isBypassEnabled, "isBypassEnabled", faceDetectLog),
logAndObserve(
- biometricSettingsRepository.isNonStrongBiometricAllowed
- .isFalse()
- .or(trustRepository.isCurrentUserTrusted),
- "nonStrongBiometricIsNotAllowedOrCurrentUserIsTrusted",
+ isFaceBiometricsAllowed.isFalse().or(trustRepository.isCurrentUserTrusted),
+ "biometricIsNotAllowedOrCurrentUserIsTrusted",
faceDetectLog
),
// We don't want to run face detect if fingerprint can be used to unlock the device
@@ -369,20 +380,11 @@ constructor(
canFaceAuthOrDetectRun(faceAuthLog),
logAndObserve(isLockedOut.isFalse(), "isNotInLockOutState", faceAuthLog),
logAndObserve(
- deviceEntryFingerprintAuthRepository.isLockedOut.isFalse(),
- "fpIsNotLockedOut",
- faceAuthLog
- ),
- logAndObserve(
trustRepository.isCurrentUserTrusted.isFalse(),
"currentUserIsNotTrusted",
faceAuthLog
),
- logAndObserve(
- biometricSettingsRepository.isNonStrongBiometricAllowed,
- "nonStrongBiometricIsAllowed",
- faceAuthLog
- ),
+ logAndObserve(isFaceBiometricsAllowed, "isFaceBiometricsAllowed", faceAuthLog),
logAndObserve(isAuthenticated.isFalse(), "faceNotAuthenticated", faceAuthLog),
)
.reduce(::and)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index 324d443d974d..40e0604ae1b3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -409,6 +409,10 @@ constructor(
KeyguardPickerFlag(
name = Contract.FlagsTable.FLAG_NAME_TRANSIT_CLOCK,
value = featureFlags.isEnabled(Flags.TRANSIT_CLOCK)
+ ),
+ KeyguardPickerFlag(
+ name = Contract.FlagsTable.FLAG_NAME_PAGE_TRANSITIONS,
+ value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_PAGE_TRANSITIONS)
)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
index 77e2847cbe76..c4749e093854 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
@@ -26,6 +26,7 @@ import android.os.VibrationEffect
import android.util.Log
import android.util.MathUtils
import android.view.Gravity
+import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.ViewConfiguration
@@ -36,6 +37,8 @@ import androidx.core.view.isVisible
import androidx.dynamicanimation.animation.DynamicAnimation
import com.android.internal.util.LatencyTracker
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
import com.android.systemui.plugins.NavigationEdgeBackPlugin
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -76,27 +79,24 @@ private const val POP_ON_INACTIVE_TO_ACTIVE_VELOCITY = 4.7f
private const val POP_ON_INACTIVE_VELOCITY = -1.5f
internal val VIBRATE_ACTIVATED_EFFECT =
- VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)
+ VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)
internal val VIBRATE_DEACTIVATED_EFFECT =
- VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)
+ VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)
private const val DEBUG = false
-class BackPanelController internal constructor(
- context: Context,
- private val windowManager: WindowManager,
- private val viewConfiguration: ViewConfiguration,
- @Main private val mainHandler: Handler,
- private val vibratorHelper: VibratorHelper,
- private val configurationController: ConfigurationController,
- private val latencyTracker: LatencyTracker
-) : ViewController<BackPanel>(
- BackPanel(
- context,
- latencyTracker
- )
-), NavigationEdgeBackPlugin {
+class BackPanelController
+internal constructor(
+ context: Context,
+ private val windowManager: WindowManager,
+ private val viewConfiguration: ViewConfiguration,
+ @Main private val mainHandler: Handler,
+ private val vibratorHelper: VibratorHelper,
+ private val configurationController: ConfigurationController,
+ private val latencyTracker: LatencyTracker,
+ private val featureFlags: FeatureFlags
+) : ViewController<BackPanel>(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin {
/**
* Injectable instance to create a new BackPanelController.
@@ -104,34 +104,37 @@ class BackPanelController internal constructor(
* Necessary because EdgeBackGestureHandler sometimes needs to create new instances of
* BackPanelController, and we need to match EdgeBackGestureHandler's context.
*/
- class Factory @Inject constructor(
- private val windowManager: WindowManager,
- private val viewConfiguration: ViewConfiguration,
- @Main private val mainHandler: Handler,
- private val vibratorHelper: VibratorHelper,
- private val configurationController: ConfigurationController,
- private val latencyTracker: LatencyTracker
+ class Factory
+ @Inject
+ constructor(
+ private val windowManager: WindowManager,
+ private val viewConfiguration: ViewConfiguration,
+ @Main private val mainHandler: Handler,
+ private val vibratorHelper: VibratorHelper,
+ private val configurationController: ConfigurationController,
+ private val latencyTracker: LatencyTracker,
+ private val featureFlags: FeatureFlags
) {
- /** Construct a [BackPanelController]. */
+ /** Construct a [BackPanelController]. */
fun create(context: Context): BackPanelController {
- val backPanelController = BackPanelController(
+ val backPanelController =
+ BackPanelController(
context,
windowManager,
viewConfiguration,
mainHandler,
vibratorHelper,
configurationController,
- latencyTracker
- )
+ latencyTracker,
+ featureFlags
+ )
backPanelController.init()
return backPanelController
}
}
- @VisibleForTesting
- internal var params: EdgePanelParams = EdgePanelParams(resources)
- @VisibleForTesting
- internal var currentState: GestureState = GestureState.GONE
+ @VisibleForTesting internal var params: EdgePanelParams = EdgePanelParams(resources)
+ @VisibleForTesting internal var currentState: GestureState = GestureState.GONE
private var previousState: GestureState = GestureState.GONE
// Screen attributes
@@ -167,7 +170,6 @@ class BackPanelController internal constructor(
private val elapsedTimeSinceEntry
get() = SystemClock.uptimeMillis() - gestureEntryTime
-
private var pastThresholdWhileEntryOrInactiveTime = 0L
private var entryToActiveDelay = 0F
private val entryToActiveDelayCalculation = {
@@ -206,24 +208,25 @@ class BackPanelController internal constructor(
COMMITTED,
/* back action currently cancelling, arrow soon to be GONE */
- CANCELLED;
+ CANCELLED
}
/**
* Wrapper around OnAnimationEndListener which runs the given runnable after a delay. The
* runnable is not called if the animation is cancelled
*/
- inner class DelayedOnAnimationEndListener internal constructor(
- private val handler: Handler,
- private val runnableDelay: Long,
- val runnable: Runnable,
+ inner class DelayedOnAnimationEndListener
+ internal constructor(
+ private val handler: Handler,
+ private val runnableDelay: Long,
+ val runnable: Runnable,
) : DynamicAnimation.OnAnimationEndListener {
override fun onAnimationEnd(
- animation: DynamicAnimation<*>,
- canceled: Boolean,
- value: Float,
- velocity: Float
+ animation: DynamicAnimation<*>,
+ canceled: Boolean,
+ value: Float,
+ velocity: Float
) {
animation.removeEndListener(this)
@@ -239,45 +242,43 @@ class BackPanelController internal constructor(
internal fun run() = runnable.run()
}
- private val onEndSetCommittedStateListener = DelayedOnAnimationEndListener(mainHandler, 0L) {
- updateArrowState(GestureState.COMMITTED)
- }
-
+ private val onEndSetCommittedStateListener =
+ DelayedOnAnimationEndListener(mainHandler, 0L) { updateArrowState(GestureState.COMMITTED) }
private val onEndSetGoneStateListener =
- DelayedOnAnimationEndListener(mainHandler, runnableDelay = 0L) {
- cancelFailsafe()
- updateArrowState(GestureState.GONE)
- }
+ DelayedOnAnimationEndListener(mainHandler, runnableDelay = 0L) {
+ cancelFailsafe()
+ updateArrowState(GestureState.GONE)
+ }
- private val onAlphaEndSetGoneStateListener = DelayedOnAnimationEndListener(mainHandler, 0L) {
- updateRestingArrowDimens()
- if (!mView.addAnimationEndListener(mView.backgroundAlpha, onEndSetGoneStateListener)) {
- scheduleFailsafe()
+ private val onAlphaEndSetGoneStateListener =
+ DelayedOnAnimationEndListener(mainHandler, 0L) {
+ updateRestingArrowDimens()
+ if (!mView.addAnimationEndListener(mView.backgroundAlpha, onEndSetGoneStateListener)) {
+ scheduleFailsafe()
+ }
}
- }
// Minimum of the screen's width or the predefined threshold
private var fullyStretchedThreshold = 0f
- /**
- * Used for initialization and configuration changes
- */
+ /** Used for initialization and configuration changes */
private fun updateConfiguration() {
params.update(resources)
mView.updateArrowPaint(params.arrowThickness)
minFlingDistance = viewConfiguration.scaledTouchSlop * 3
}
- private val configurationListener = object : ConfigurationController.ConfigurationListener {
- override fun onConfigChanged(newConfig: Configuration?) {
- updateConfiguration()
- }
+ private val configurationListener =
+ object : ConfigurationController.ConfigurationListener {
+ override fun onConfigChanged(newConfig: Configuration?) {
+ updateConfiguration()
+ }
- override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) {
- updateArrowDirection(isLayoutRtl)
+ override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) {
+ updateArrowDirection(isLayoutRtl)
+ }
}
- }
override fun onViewAttached() {
updateConfiguration()
@@ -320,8 +321,9 @@ class BackPanelController internal constructor(
MotionEvent.ACTION_UP -> {
when (currentState) {
GestureState.ENTRY -> {
- if (isFlungAwayFromEdge(endX = event.x) ||
- previousXTranslation > params.staticTriggerThreshold
+ if (
+ isFlungAwayFromEdge(endX = event.x) ||
+ previousXTranslation > params.staticTriggerThreshold
) {
updateArrowState(GestureState.FLUNG)
} else {
@@ -342,14 +344,16 @@ class BackPanelController internal constructor(
}
}
GestureState.ACTIVE -> {
- if (previousState == GestureState.ENTRY &&
- elapsedTimeSinceEntry
- < MIN_DURATION_ENTRY_TO_ACTIVE_CONSIDERED_AS_FLING
+ if (
+ previousState == GestureState.ENTRY &&
+ elapsedTimeSinceEntry <
+ MIN_DURATION_ENTRY_TO_ACTIVE_CONSIDERED_AS_FLING
) {
updateArrowState(GestureState.FLUNG)
- } else if (previousState == GestureState.INACTIVE &&
- elapsedTimeSinceInactive
- < MIN_DURATION_INACTIVE_TO_ACTIVE_CONSIDERED_AS_FLING
+ } else if (
+ previousState == GestureState.INACTIVE &&
+ elapsedTimeSinceInactive <
+ MIN_DURATION_INACTIVE_TO_ACTIVE_CONSIDERED_AS_FLING
) {
// A delay is added to allow the background to transition back to ACTIVE
// since it was briefly in INACTIVE. Without this delay, setting it
@@ -390,10 +394,10 @@ class BackPanelController internal constructor(
}
/**
- * Returns false until the current gesture exceeds the touch slop threshold,
- * and returns true thereafter (we reset on the subsequent back gesture).
- * The moment it switches from false -> true is important,
- * because that's when we switch state, from GONE -> ENTRY.
+ * Returns false until the current gesture exceeds the touch slop threshold, and returns true
+ * thereafter (we reset on the subsequent back gesture). The moment it switches from false ->
+ * true is important, because that's when we switch state, from GONE -> ENTRY.
+ *
* @return whether the current gesture has moved past a minimum threshold.
*/
private fun dragSlopExceeded(curX: Float, startX: Float): Boolean {
@@ -416,7 +420,8 @@ class BackPanelController internal constructor(
val isPastStaticThreshold = xTranslation > params.staticTriggerThreshold
when (currentState) {
GestureState.ENTRY -> {
- if (isPastThresholdToActive(
+ if (
+ isPastThresholdToActive(
isPastThreshold = isPastStaticThreshold,
dynamicDelay = entryToActiveDelayCalculation
)
@@ -428,8 +433,10 @@ class BackPanelController internal constructor(
val isPastDynamicReactivationThreshold =
totalTouchDeltaInactive >= params.reactivationTriggerThreshold
- if (isPastThresholdToActive(
- isPastThreshold = isPastStaticThreshold &&
+ if (
+ isPastThresholdToActive(
+ isPastThreshold =
+ isPastStaticThreshold &&
isPastDynamicReactivationThreshold &&
isWithinYActivationThreshold,
delay = MIN_DURATION_INACTIVE_BEFORE_ACTIVE_ANIMATION
@@ -489,19 +496,19 @@ class BackPanelController internal constructor(
// Add a slop to to prevent small jitters when arrow is at edge in
// emitting small values that cause the arrow to poke out slightly
val minimumDelta = -viewConfiguration.scaledTouchSlop.toFloat()
- totalTouchDeltaInactive = totalTouchDeltaInactive
- .plus(xDelta)
- .coerceAtLeast(minimumDelta)
+ totalTouchDeltaInactive =
+ totalTouchDeltaInactive.plus(xDelta).coerceAtLeast(minimumDelta)
}
updateArrowStateOnMove(yTranslation, xTranslation)
- val gestureProgress = when (currentState) {
- GestureState.ACTIVE -> fullScreenProgress(xTranslation)
- GestureState.ENTRY -> staticThresholdProgress(xTranslation)
- GestureState.INACTIVE -> reactivationThresholdProgress(totalTouchDeltaInactive)
- else -> null
- }
+ val gestureProgress =
+ when (currentState) {
+ GestureState.ACTIVE -> fullScreenProgress(xTranslation)
+ GestureState.ENTRY -> staticThresholdProgress(xTranslation)
+ GestureState.INACTIVE -> reactivationThresholdProgress(totalTouchDeltaInactive)
+ else -> null
+ }
gestureProgress?.let {
when (currentState) {
@@ -517,27 +524,30 @@ class BackPanelController internal constructor(
}
private fun setArrowStrokeAlpha(gestureProgress: Float?) {
- val strokeAlphaProgress = when (currentState) {
- GestureState.ENTRY -> gestureProgress
- GestureState.INACTIVE -> gestureProgress
- GestureState.ACTIVE,
- GestureState.FLUNG,
- GestureState.COMMITTED -> 1f
- GestureState.CANCELLED,
- GestureState.GONE -> 0f
- }
+ val strokeAlphaProgress =
+ when (currentState) {
+ GestureState.ENTRY -> gestureProgress
+ GestureState.INACTIVE -> gestureProgress
+ GestureState.ACTIVE,
+ GestureState.FLUNG,
+ GestureState.COMMITTED -> 1f
+ GestureState.CANCELLED,
+ GestureState.GONE -> 0f
+ }
- val indicator = when (currentState) {
- GestureState.ENTRY -> params.entryIndicator
- GestureState.INACTIVE -> params.preThresholdIndicator
- GestureState.ACTIVE -> params.activeIndicator
- else -> params.preThresholdIndicator
- }
+ val indicator =
+ when (currentState) {
+ GestureState.ENTRY -> params.entryIndicator
+ GestureState.INACTIVE -> params.preThresholdIndicator
+ GestureState.ACTIVE -> params.activeIndicator
+ else -> params.preThresholdIndicator
+ }
strokeAlphaProgress?.let { progress ->
- indicator.arrowDimens.alphaSpring?.get(progress)?.takeIf { it.isNewState }?.let {
- mView.popArrowAlpha(0f, it.value)
- }
+ indicator.arrowDimens.alphaSpring
+ ?.get(progress)
+ ?.takeIf { it.isNewState }
+ ?.let { mView.popArrowAlpha(0f, it.value) }
}
}
@@ -546,15 +556,16 @@ class BackPanelController internal constructor(
val maxYOffset = (mView.height - params.entryIndicator.backgroundDimens.height) / 2f
val rubberbandAmount = 15f
val yProgress = MathUtils.saturate(yTranslation / (maxYOffset * rubberbandAmount))
- val yPosition = params.verticalTranslationInterpolator.getInterpolation(yProgress) *
+ val yPosition =
+ params.verticalTranslationInterpolator.getInterpolation(yProgress) *
maxYOffset *
sign(yOffset)
mView.animateVertically(yPosition)
}
/**
- * Tracks the relative position of the drag from the time after the arrow is activated until
- * the arrow is fully stretched (between 0.0 - 1.0f)
+ * Tracks the relative position of the drag from the time after the arrow is activated until the
+ * arrow is fully stretched (between 0.0 - 1.0f)
*/
private fun fullScreenProgress(xTranslation: Float): Float {
val progress = (xTranslation - previousXTranslationOnActiveOffset) / fullyStretchedThreshold
@@ -575,35 +586,32 @@ class BackPanelController internal constructor(
private fun stretchActiveBackIndicator(progress: Float) {
mView.setStretch(
- horizontalTranslationStretchAmount = params.horizontalTranslationInterpolator
- .getInterpolation(progress),
- arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
- backgroundWidthStretchAmount = params.activeWidthInterpolator
- .getInterpolation(progress),
- backgroundAlphaStretchAmount = 1f,
- backgroundHeightStretchAmount = 1f,
- arrowAlphaStretchAmount = 1f,
- edgeCornerStretchAmount = 1f,
- farCornerStretchAmount = 1f,
- fullyStretchedDimens = params.fullyStretchedIndicator
+ horizontalTranslationStretchAmount =
+ params.horizontalTranslationInterpolator.getInterpolation(progress),
+ arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
+ backgroundWidthStretchAmount =
+ params.activeWidthInterpolator.getInterpolation(progress),
+ backgroundAlphaStretchAmount = 1f,
+ backgroundHeightStretchAmount = 1f,
+ arrowAlphaStretchAmount = 1f,
+ edgeCornerStretchAmount = 1f,
+ farCornerStretchAmount = 1f,
+ fullyStretchedDimens = params.fullyStretchedIndicator
)
}
private fun stretchEntryBackIndicator(progress: Float) {
mView.setStretch(
- horizontalTranslationStretchAmount = 0f,
- arrowStretchAmount = params.arrowAngleInterpolator
- .getInterpolation(progress),
- backgroundWidthStretchAmount = params.entryWidthInterpolator
- .getInterpolation(progress),
- backgroundHeightStretchAmount = params.heightInterpolator
- .getInterpolation(progress),
- backgroundAlphaStretchAmount = 1f,
- arrowAlphaStretchAmount = params.entryIndicator.arrowDimens
- .alphaInterpolator?.get(progress)?.value ?: 0f,
- edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
- farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
- fullyStretchedDimens = params.preThresholdIndicator
+ horizontalTranslationStretchAmount = 0f,
+ arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
+ backgroundWidthStretchAmount = params.entryWidthInterpolator.getInterpolation(progress),
+ backgroundHeightStretchAmount = params.heightInterpolator.getInterpolation(progress),
+ backgroundAlphaStretchAmount = 1f,
+ arrowAlphaStretchAmount =
+ params.entryIndicator.arrowDimens.alphaInterpolator?.get(progress)?.value ?: 0f,
+ edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
+ farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
+ fullyStretchedDimens = params.preThresholdIndicator
)
}
@@ -612,31 +620,32 @@ class BackPanelController internal constructor(
val interpolator = run {
val isPastSlop = totalTouchDeltaInactive > viewConfiguration.scaledTouchSlop
if (isPastSlop) {
- if (totalTouchDeltaInactive > 0) {
- params.entryWidthInterpolator
+ if (totalTouchDeltaInactive > 0) {
+ params.entryWidthInterpolator
+ } else {
+ params.entryWidthTowardsEdgeInterpolator
+ }
} else {
- params.entryWidthTowardsEdgeInterpolator
+ previousPreThresholdWidthInterpolator
}
- } else {
- previousPreThresholdWidthInterpolator
- }.also { previousPreThresholdWidthInterpolator = it }
+ .also { previousPreThresholdWidthInterpolator = it }
}
return interpolator.getInterpolation(progress).coerceAtLeast(0f)
}
private fun stretchInactiveBackIndicator(progress: Float) {
mView.setStretch(
- horizontalTranslationStretchAmount = 0f,
- arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
- backgroundWidthStretchAmount = preThresholdWidthStretchAmount(progress),
- backgroundHeightStretchAmount = params.heightInterpolator
- .getInterpolation(progress),
- backgroundAlphaStretchAmount = 1f,
- arrowAlphaStretchAmount = params.preThresholdIndicator.arrowDimens
- .alphaInterpolator?.get(progress)?.value ?: 0f,
- edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
- farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
- fullyStretchedDimens = params.preThresholdIndicator
+ horizontalTranslationStretchAmount = 0f,
+ arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
+ backgroundWidthStretchAmount = preThresholdWidthStretchAmount(progress),
+ backgroundHeightStretchAmount = params.heightInterpolator.getInterpolation(progress),
+ backgroundAlphaStretchAmount = 1f,
+ arrowAlphaStretchAmount =
+ params.preThresholdIndicator.arrowDimens.alphaInterpolator?.get(progress)?.value
+ ?: 0f,
+ edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
+ farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
+ fullyStretchedDimens = params.preThresholdIndicator
)
}
@@ -647,11 +656,12 @@ class BackPanelController internal constructor(
override fun setIsLeftPanel(isLeftPanel: Boolean) {
mView.isLeftPanel = isLeftPanel
- layoutParams.gravity = if (isLeftPanel) {
- Gravity.LEFT or Gravity.TOP
- } else {
- Gravity.RIGHT or Gravity.TOP
- }
+ layoutParams.gravity =
+ if (isLeftPanel) {
+ Gravity.LEFT or Gravity.TOP
+ } else {
+ Gravity.RIGHT or Gravity.TOP
+ }
}
override fun setInsets(insetLeft: Int, insetRight: Int) = Unit
@@ -667,12 +677,14 @@ class BackPanelController internal constructor(
private fun isFlungAwayFromEdge(endX: Float, startX: Float = touchDeltaStartX): Boolean {
val flingDistance = if (mView.isLeftPanel) endX - startX else startX - endX
- val flingVelocity = velocityTracker?.run {
- computeCurrentVelocity(PX_PER_SEC)
- xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1)
- } ?: 0f
+ val flingVelocity =
+ velocityTracker?.run {
+ computeCurrentVelocity(PX_PER_SEC)
+ xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1)
+ }
+ ?: 0f
val isPastFlingVelocityThreshold =
- flingVelocity > viewConfiguration.scaledMinimumFlingVelocity
+ flingVelocity > viewConfiguration.scaledMinimumFlingVelocity
return flingDistance > minFlingDistance && isPastFlingVelocityThreshold
}
@@ -699,8 +711,8 @@ class BackPanelController internal constructor(
}
private fun playWithBackgroundWidthAnimation(
- onEnd: DelayedOnAnimationEndListener,
- delay: Long = 0L
+ onEnd: DelayedOnAnimationEndListener,
+ delay: Long = 0L
) {
if (delay == 0L) {
updateRestingArrowDimens()
@@ -724,104 +736,103 @@ class BackPanelController internal constructor(
fullyStretchedThreshold = min(displaySize.x.toFloat(), params.swipeProgressThreshold)
}
- /**
- * Updates resting arrow and background size not accounting for stretch
- */
+ /** Updates resting arrow and background size not accounting for stretch */
private fun updateRestingArrowDimens() {
when (currentState) {
GestureState.GONE,
GestureState.ENTRY -> {
mView.setSpring(
- arrowLength = params.entryIndicator.arrowDimens.lengthSpring,
- arrowHeight = params.entryIndicator.arrowDimens.heightSpring,
- scale = params.entryIndicator.scaleSpring,
- verticalTranslation = params.entryIndicator.verticalTranslationSpring,
- horizontalTranslation = params.entryIndicator.horizontalTranslationSpring,
- backgroundAlpha = params.entryIndicator.backgroundDimens.alphaSpring,
- backgroundWidth = params.entryIndicator.backgroundDimens.widthSpring,
- backgroundHeight = params.entryIndicator.backgroundDimens.heightSpring,
- backgroundEdgeCornerRadius = params.entryIndicator.backgroundDimens
- .edgeCornerRadiusSpring,
- backgroundFarCornerRadius = params.entryIndicator.backgroundDimens
- .farCornerRadiusSpring,
+ arrowLength = params.entryIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.entryIndicator.arrowDimens.heightSpring,
+ scale = params.entryIndicator.scaleSpring,
+ verticalTranslation = params.entryIndicator.verticalTranslationSpring,
+ horizontalTranslation = params.entryIndicator.horizontalTranslationSpring,
+ backgroundAlpha = params.entryIndicator.backgroundDimens.alphaSpring,
+ backgroundWidth = params.entryIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.entryIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius =
+ params.entryIndicator.backgroundDimens.edgeCornerRadiusSpring,
+ backgroundFarCornerRadius =
+ params.entryIndicator.backgroundDimens.farCornerRadiusSpring,
)
}
GestureState.INACTIVE -> {
mView.setSpring(
- arrowLength = params.preThresholdIndicator.arrowDimens.lengthSpring,
- arrowHeight = params.preThresholdIndicator.arrowDimens.heightSpring,
- horizontalTranslation = params.preThresholdIndicator
- .horizontalTranslationSpring,
- scale = params.preThresholdIndicator.scaleSpring,
- backgroundWidth = params.preThresholdIndicator.backgroundDimens
- .widthSpring,
- backgroundHeight = params.preThresholdIndicator.backgroundDimens
- .heightSpring,
- backgroundEdgeCornerRadius = params.preThresholdIndicator.backgroundDimens
- .edgeCornerRadiusSpring,
- backgroundFarCornerRadius = params.preThresholdIndicator.backgroundDimens
- .farCornerRadiusSpring,
+ arrowLength = params.preThresholdIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.preThresholdIndicator.arrowDimens.heightSpring,
+ horizontalTranslation =
+ params.preThresholdIndicator.horizontalTranslationSpring,
+ scale = params.preThresholdIndicator.scaleSpring,
+ backgroundWidth = params.preThresholdIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.preThresholdIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius =
+ params.preThresholdIndicator.backgroundDimens.edgeCornerRadiusSpring,
+ backgroundFarCornerRadius =
+ params.preThresholdIndicator.backgroundDimens.farCornerRadiusSpring,
)
}
GestureState.ACTIVE -> {
mView.setSpring(
- arrowLength = params.activeIndicator.arrowDimens.lengthSpring,
- arrowHeight = params.activeIndicator.arrowDimens.heightSpring,
- scale = params.activeIndicator.scaleSpring,
- horizontalTranslation = params.activeIndicator.horizontalTranslationSpring,
- backgroundWidth = params.activeIndicator.backgroundDimens.widthSpring,
- backgroundHeight = params.activeIndicator.backgroundDimens.heightSpring,
- backgroundEdgeCornerRadius = params.activeIndicator.backgroundDimens
- .edgeCornerRadiusSpring,
- backgroundFarCornerRadius = params.activeIndicator.backgroundDimens
- .farCornerRadiusSpring,
+ arrowLength = params.activeIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.activeIndicator.arrowDimens.heightSpring,
+ scale = params.activeIndicator.scaleSpring,
+ horizontalTranslation = params.activeIndicator.horizontalTranslationSpring,
+ backgroundWidth = params.activeIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.activeIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius =
+ params.activeIndicator.backgroundDimens.edgeCornerRadiusSpring,
+ backgroundFarCornerRadius =
+ params.activeIndicator.backgroundDimens.farCornerRadiusSpring,
)
}
GestureState.FLUNG -> {
mView.setSpring(
- arrowLength = params.flungIndicator.arrowDimens.lengthSpring,
- arrowHeight = params.flungIndicator.arrowDimens.heightSpring,
- backgroundWidth = params.flungIndicator.backgroundDimens.widthSpring,
- backgroundHeight = params.flungIndicator.backgroundDimens.heightSpring,
- backgroundEdgeCornerRadius = params.flungIndicator.backgroundDimens
- .edgeCornerRadiusSpring,
- backgroundFarCornerRadius = params.flungIndicator.backgroundDimens
- .farCornerRadiusSpring,
+ arrowLength = params.flungIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.flungIndicator.arrowDimens.heightSpring,
+ backgroundWidth = params.flungIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.flungIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius =
+ params.flungIndicator.backgroundDimens.edgeCornerRadiusSpring,
+ backgroundFarCornerRadius =
+ params.flungIndicator.backgroundDimens.farCornerRadiusSpring,
)
}
GestureState.COMMITTED -> {
mView.setSpring(
- arrowLength = params.committedIndicator.arrowDimens.lengthSpring,
- arrowHeight = params.committedIndicator.arrowDimens.heightSpring,
- scale = params.committedIndicator.scaleSpring,
- backgroundAlpha = params.committedIndicator.backgroundDimens.alphaSpring,
- backgroundWidth = params.committedIndicator.backgroundDimens.widthSpring,
- backgroundHeight = params.committedIndicator.backgroundDimens.heightSpring,
- backgroundEdgeCornerRadius = params.committedIndicator.backgroundDimens
- .edgeCornerRadiusSpring,
- backgroundFarCornerRadius = params.committedIndicator.backgroundDimens
- .farCornerRadiusSpring,
+ arrowLength = params.committedIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.committedIndicator.arrowDimens.heightSpring,
+ scale = params.committedIndicator.scaleSpring,
+ backgroundAlpha = params.committedIndicator.backgroundDimens.alphaSpring,
+ backgroundWidth = params.committedIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.committedIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius =
+ params.committedIndicator.backgroundDimens.edgeCornerRadiusSpring,
+ backgroundFarCornerRadius =
+ params.committedIndicator.backgroundDimens.farCornerRadiusSpring,
)
}
GestureState.CANCELLED -> {
mView.setSpring(
- backgroundAlpha = params.cancelledIndicator.backgroundDimens.alphaSpring)
+ backgroundAlpha = params.cancelledIndicator.backgroundDimens.alphaSpring
+ )
}
else -> {}
}
mView.setRestingDimens(
- animate = !(currentState == GestureState.FLUNG ||
- currentState == GestureState.COMMITTED),
- restingParams = EdgePanelParams.BackIndicatorDimens(
- scale = when (currentState) {
+ animate =
+ !(currentState == GestureState.FLUNG || currentState == GestureState.COMMITTED),
+ restingParams =
+ EdgePanelParams.BackIndicatorDimens(
+ scale =
+ when (currentState) {
GestureState.ACTIVE,
- GestureState.FLUNG,
- -> params.activeIndicator.scale
+ GestureState.FLUNG, -> params.activeIndicator.scale
GestureState.COMMITTED -> params.committedIndicator.scale
else -> params.preThresholdIndicator.scale
},
- scalePivotX = when (currentState) {
+ scalePivotX =
+ when (currentState) {
GestureState.GONE,
GestureState.ENTRY,
GestureState.INACTIVE,
@@ -830,7 +841,8 @@ class BackPanelController internal constructor(
GestureState.FLUNG,
GestureState.COMMITTED -> params.committedIndicator.scalePivotX
},
- horizontalTranslation = when (currentState) {
+ horizontalTranslation =
+ when (currentState) {
GestureState.GONE -> {
params.activeIndicator.backgroundDimens.width?.times(-1)
}
@@ -843,7 +855,8 @@ class BackPanelController internal constructor(
}
else -> null
},
- arrowDimens = when (currentState) {
+ arrowDimens =
+ when (currentState) {
GestureState.GONE,
GestureState.ENTRY,
GestureState.INACTIVE -> params.entryIndicator.arrowDimens
@@ -852,7 +865,8 @@ class BackPanelController internal constructor(
GestureState.COMMITTED -> params.committedIndicator.arrowDimens
GestureState.CANCELLED -> params.cancelledIndicator.arrowDimens
},
- backgroundDimens = when (currentState) {
+ backgroundDimens =
+ when (currentState) {
GestureState.GONE,
GestureState.ENTRY,
GestureState.INACTIVE -> params.entryIndicator.backgroundDimens
@@ -894,7 +908,7 @@ class BackPanelController internal constructor(
GestureState.ACTIVE -> {
backCallback.setTriggerBack(true)
}
- GestureState.GONE -> { }
+ GestureState.GONE -> {}
}
when (currentState) {
@@ -913,18 +927,25 @@ class BackPanelController internal constructor(
GestureState.ACTIVE -> {
previousXTranslationOnActiveOffset = previousXTranslation
updateRestingArrowDimens()
- vibratorHelper.cancel()
- mainHandler.postDelayed(10L) {
- vibratorHelper.vibrate(VIBRATE_ACTIVATED_EFFECT)
- }
- val popVelocity = if (previousState == GestureState.INACTIVE) {
- POP_ON_INACTIVE_TO_ACTIVE_VELOCITY
+ if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+ vibratorHelper.performHapticFeedback(
+ mView,
+ HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE
+ )
} else {
- POP_ON_ENTRY_TO_ACTIVE_VELOCITY
+ vibratorHelper.cancel()
+ mainHandler.postDelayed(10L) {
+ vibratorHelper.vibrate(VIBRATE_ACTIVATED_EFFECT)
+ }
}
+ val popVelocity =
+ if (previousState == GestureState.INACTIVE) {
+ POP_ON_INACTIVE_TO_ACTIVE_VELOCITY
+ } else {
+ POP_ON_ENTRY_TO_ACTIVE_VELOCITY
+ }
mView.popOffEdge(popVelocity)
}
-
GestureState.INACTIVE -> {
gestureInactiveTime = SystemClock.uptimeMillis()
@@ -937,7 +958,14 @@ class BackPanelController internal constructor(
mView.popOffEdge(POP_ON_INACTIVE_VELOCITY)
- vibratorHelper.vibrate(VIBRATE_DEACTIVATED_EFFECT)
+ if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+ vibratorHelper.performHapticFeedback(
+ mView,
+ HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE
+ )
+ } else {
+ vibratorHelper.vibrate(VIBRATE_DEACTIVATED_EFFECT)
+ }
updateRestingArrowDimens()
}
GestureState.FLUNG -> {
@@ -945,8 +973,10 @@ class BackPanelController internal constructor(
mView.popScale(POP_ON_FLING_VELOCITY)
}
updateRestingArrowDimens()
- mainHandler.postDelayed(onEndSetCommittedStateListener.runnable,
- MIN_DURATION_FLING_ANIMATION)
+ mainHandler.postDelayed(
+ onEndSetCommittedStateListener.runnable,
+ MIN_DURATION_FLING_ANIMATION
+ )
}
GestureState.COMMITTED -> {
// In most cases, animating between states is handled via `updateRestingArrowDimens`
@@ -956,36 +986,43 @@ class BackPanelController internal constructor(
// manually play these kinds of animations in parallel.
if (previousState == GestureState.FLUNG) {
updateRestingArrowDimens()
- mainHandler.postDelayed(onEndSetGoneStateListener.runnable,
- MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION)
+ mainHandler.postDelayed(
+ onEndSetGoneStateListener.runnable,
+ MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION
+ )
} else {
mView.popScale(POP_ON_COMMITTED_VELOCITY)
- mainHandler.postDelayed(onAlphaEndSetGoneStateListener.runnable,
- MIN_DURATION_COMMITTED_ANIMATION)
+ mainHandler.postDelayed(
+ onAlphaEndSetGoneStateListener.runnable,
+ MIN_DURATION_COMMITTED_ANIMATION
+ )
}
}
GestureState.CANCELLED -> {
val delay = max(0, MIN_DURATION_CANCELLED_ANIMATION - elapsedTimeSinceEntry)
playWithBackgroundWidthAnimation(onEndSetGoneStateListener, delay)
- val springForceOnCancelled = params.cancelledIndicator
- .arrowDimens.alphaSpring?.get(0f)?.value
+ val springForceOnCancelled =
+ params.cancelledIndicator.arrowDimens.alphaSpring?.get(0f)?.value
mView.popArrowAlpha(0f, springForceOnCancelled)
- mainHandler.postDelayed(10L) { vibratorHelper.cancel() }
+ if (!featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION))
+ mainHandler.postDelayed(10L) { vibratorHelper.cancel() }
}
}
}
private fun convertVelocityToAnimationFactor(
- valueOnFastVelocity: Float,
- valueOnSlowVelocity: Float,
- fastVelocityBound: Float = 1f,
- slowVelocityBound: Float = 0.5f,
+ valueOnFastVelocity: Float,
+ valueOnSlowVelocity: Float,
+ fastVelocityBound: Float = 1f,
+ slowVelocityBound: Float = 0.5f,
): Float {
- val factor = velocityTracker?.run {
- computeCurrentVelocity(PX_PER_MS)
- MathUtils.smoothStep(slowVelocityBound, fastVelocityBound, abs(xVelocity))
- } ?: valueOnFastVelocity
+ val factor =
+ velocityTracker?.run {
+ computeCurrentVelocity(PX_PER_MS)
+ MathUtils.smoothStep(slowVelocityBound, fastVelocityBound, abs(xVelocity))
+ }
+ ?: valueOnFastVelocity
return MathUtils.lerp(valueOnFastVelocity, valueOnSlowVelocity, 1 - factor)
}
@@ -1014,77 +1051,76 @@ class BackPanelController internal constructor(
}
init {
- if (DEBUG) mView.drawDebugInfo = { canvas ->
- val debugStrings = listOf(
- "$currentState",
- "startX=$startX",
- "startY=$startY",
- "xDelta=${"%.1f".format(totalTouchDeltaActive)}",
- "xTranslation=${"%.1f".format(previousXTranslation)}",
- "pre=${"%.0f".format(staticThresholdProgress(previousXTranslation) * 100)}%",
- "post=${"%.0f".format(fullScreenProgress(previousXTranslation) * 100)}%"
- )
- val debugPaint = Paint().apply {
- color = Color.WHITE
- }
- val debugInfoBottom = debugStrings.size * 32f + 4f
- canvas.drawRect(
+ if (DEBUG)
+ mView.drawDebugInfo = { canvas ->
+ val preProgress = staticThresholdProgress(previousXTranslation) * 100
+ val postProgress = fullScreenProgress(previousXTranslation) * 100
+ val debugStrings =
+ listOf(
+ "$currentState",
+ "startX=$startX",
+ "startY=$startY",
+ "xDelta=${"%.1f".format(totalTouchDeltaActive)}",
+ "xTranslation=${"%.1f".format(previousXTranslation)}",
+ "pre=${"%.0f".format(preProgress)}%",
+ "post=${"%.0f".format(postProgress)}%"
+ )
+ val debugPaint = Paint().apply { color = Color.WHITE }
+ val debugInfoBottom = debugStrings.size * 32f + 4f
+ canvas.drawRect(
4f,
4f,
canvas.width.toFloat(),
debugStrings.size * 32f + 4f,
debugPaint
- )
- debugPaint.apply {
- color = Color.BLACK
- textSize = 32f
- }
- var offset = 32f
- for (debugText in debugStrings) {
- canvas.drawText(debugText, 10f, offset, debugPaint)
- offset += 32f
- }
- debugPaint.apply {
- color = Color.RED
- style = Paint.Style.STROKE
- strokeWidth = 4f
- }
- val canvasWidth = canvas.width.toFloat()
- val canvasHeight = canvas.height.toFloat()
- canvas.drawRect(0f, 0f, canvasWidth, canvasHeight, debugPaint)
-
- fun drawVerticalLine(x: Float, color: Int) {
- debugPaint.color = color
- val x = if (mView.isLeftPanel) x else canvasWidth - x
- canvas.drawLine(x, debugInfoBottom, x, canvas.height.toFloat(), debugPaint)
- }
+ )
+ debugPaint.apply {
+ color = Color.BLACK
+ textSize = 32f
+ }
+ var offset = 32f
+ for (debugText in debugStrings) {
+ canvas.drawText(debugText, 10f, offset, debugPaint)
+ offset += 32f
+ }
+ debugPaint.apply {
+ color = Color.RED
+ style = Paint.Style.STROKE
+ strokeWidth = 4f
+ }
+ val canvasWidth = canvas.width.toFloat()
+ val canvasHeight = canvas.height.toFloat()
+ canvas.drawRect(0f, 0f, canvasWidth, canvasHeight, debugPaint)
+
+ fun drawVerticalLine(x: Float, color: Int) {
+ debugPaint.color = color
+ val x = if (mView.isLeftPanel) x else canvasWidth - x
+ canvas.drawLine(x, debugInfoBottom, x, canvas.height.toFloat(), debugPaint)
+ }
- drawVerticalLine(x = params.staticTriggerThreshold, color = Color.BLUE)
- drawVerticalLine(x = params.deactivationTriggerThreshold, color = Color.BLUE)
- drawVerticalLine(x = startX, color = Color.GREEN)
- drawVerticalLine(x = previousXTranslation, color = Color.DKGRAY)
- }
+ drawVerticalLine(x = params.staticTriggerThreshold, color = Color.BLUE)
+ drawVerticalLine(x = params.deactivationTriggerThreshold, color = Color.BLUE)
+ drawVerticalLine(x = startX, color = Color.GREEN)
+ drawVerticalLine(x = previousXTranslation, color = Color.DKGRAY)
+ }
}
}
/**
- * In addition to a typical step function which returns one or two
- * values based on a threshold, `Step` also gracefully handles quick
- * changes in input near the threshold value that would typically
- * result in the output rapidly changing.
+ * In addition to a typical step function which returns one or two values based on a threshold,
+ * `Step` also gracefully handles quick changes in input near the threshold value that would
+ * typically result in the output rapidly changing.
*
- * In the context of Back arrow, the arrow's stroke opacity should
- * always appear transparent or opaque. Using a typical Step function,
- * this would resulting in a flickering appearance as the output would
- * change rapidly. `Step` addresses this by moving the threshold after
- * it is crossed so it cannot be easily crossed again with small changes
- * in touch events.
+ * In the context of Back arrow, the arrow's stroke opacity should always appear transparent or
+ * opaque. Using a typical Step function, this would resulting in a flickering appearance as the
+ * output would change rapidly. `Step` addresses this by moving the threshold after it is crossed so
+ * it cannot be easily crossed again with small changes in touch events.
*/
class Step<T>(
- private val threshold: Float,
- private val factor: Float = 1.1f,
- private val postThreshold: T,
- private val preThreshold: T
+ private val threshold: Float,
+ private val factor: Float = 1.1f,
+ private val postThreshold: T,
+ private val preThreshold: T
) {
data class Value<T>(val value: T, val isNewState: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index 48790c23e688..2adc211ef23f 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -41,6 +41,8 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.devicepolicy.areKeyguardShortcutsDisabled
import com.android.systemui.log.DebugLogger.debugLog
+import com.android.systemui.notetask.NoteTaskEntryPoint.QUICK_AFFORDANCE
+import com.android.systemui.notetask.NoteTaskEntryPoint.TAIL_BUTTON
import com.android.systemui.notetask.NoteTaskRoleManagerExt.createNoteShortcutInfoAsUser
import com.android.systemui.notetask.NoteTaskRoleManagerExt.getDefaultRoleHolderAsUser
import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
@@ -121,23 +123,26 @@ constructor(
/**
* Returns the [UserHandle] of an android user that should handle the notes taking [entryPoint].
- *
- * On company owned personally enabled (COPE) devices, if the given [entryPoint] is in the
- * [FORCE_WORK_NOTE_APPS_ENTRY_POINTS_ON_COPE_DEVICES] list, the default notes app in the work
- * profile user will always be launched.
- *
- * On non managed devices or devices with other management modes, the current [UserHandle] is
- * returned.
+ * 1. tail button entry point: In COPE or work profile devices, the user can select whether the
+ * work or main profile notes app should be launched in the Settings app. In non-management
+ * or device owner devices, the user can only select main profile notes app.
+ * 2. lock screen quick affordance: since there is no user setting, the main profile notes app
+ * is used as default for work profile devices while the work profile notes app is used for
+ * COPE devices.
+ * 3. Other entry point: the current user from [UserTracker.userHandle].
*/
fun getUserForHandlingNotesTaking(entryPoint: NoteTaskEntryPoint): UserHandle =
- if (
- entryPoint in FORCE_WORK_NOTE_APPS_ENTRY_POINTS_ON_COPE_DEVICES &&
- devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile
- ) {
- userTracker.userProfiles.firstOrNull { userManager.isManagedProfile(it.id) }?.userHandle
- ?: userTracker.userHandle
- } else {
- secureSettings.preferredUser
+ when {
+ entryPoint == TAIL_BUTTON -> secureSettings.preferredUser
+ devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile &&
+ entryPoint == QUICK_AFFORDANCE -> {
+ userTracker.userProfiles
+ .firstOrNull { userManager.isManagedProfile(it.id) }
+ ?.userHandle
+ ?: userTracker.userHandle
+ }
+ // On work profile devices, SysUI always run in the main user.
+ else -> userTracker.userHandle
}
/**
@@ -267,15 +272,7 @@ constructor(
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
}
- // If the required user matches the tracking user, the injected context is already a context
- // of the required user. Avoid calling #createContextAsUser because creating a context for
- // a user takes time.
- val userContext =
- if (user == userTracker.userHandle) {
- context
- } else {
- context.createContextAsUser(user, /* flags= */ 0)
- }
+ val userContext = context.createContextAsUser(user, /* flags= */ 0)
userContext.packageManager.setComponentEnabledSetting(
componentName,
@@ -283,7 +280,7 @@ constructor(
PackageManager.DONT_KILL_APP,
)
- debugLog { "setNoteTaskShortcutEnabled - completed: $isEnabled" }
+ debugLog { "setNoteTaskShortcutEnabled for user $user- completed: $enabledState" }
}
/**
@@ -359,10 +356,12 @@ constructor(
private val SecureSettings.preferredUser: UserHandle
get() {
+ val trackingUserId = userTracker.userHandle.identifier
val userId =
- secureSettings.getInt(
- Settings.Secure.DEFAULT_NOTE_TASK_PROFILE,
- userTracker.userHandle.identifier,
+ secureSettings.getIntForUser(
+ /* name= */ Settings.Secure.DEFAULT_NOTE_TASK_PROFILE,
+ /* def= */ trackingUserId,
+ /* userHandle= */ trackingUserId,
)
return UserHandle.of(userId)
}
@@ -381,16 +380,6 @@ constructor(
* @see com.android.launcher3.icons.IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE
*/
const val EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE = "extra_shortcut_badge_override_package"
-
- /**
- * A list of entry points which should be redirected to the work profile default notes app
- * on company owned personally enabled (COPE) devices.
- *
- * Entry points in this list don't let users / admin to select the work or personal default
- * notes app to be launched.
- */
- val FORCE_WORK_NOTE_APPS_ENTRY_POINTS_ON_COPE_DEVICES =
- listOf(NoteTaskEntryPoint.TAIL_BUTTON, NoteTaskEntryPoint.QUICK_AFFORDANCE)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt
new file mode 100644
index 000000000000..fdc70a83e8b1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt
@@ -0,0 +1,367 @@
+/*
+ * Copyright (C) 2023 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.privacy
+
+import android.Manifest
+import android.app.ActivityManager
+import android.app.Dialog
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.UserHandle
+import android.permission.PermissionGroupUsage
+import android.permission.PermissionManager
+import android.view.View
+import androidx.annotation.MainThread
+import androidx.annotation.WorkerThread
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.appops.AppOpsController
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.privacy.logging.PrivacyLogger
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+private val defaultDialogProvider =
+ object : PrivacyDialogControllerV2.DialogProvider {
+ override fun makeDialog(
+ context: Context,
+ list: List<PrivacyDialogV2.PrivacyElement>,
+ manageApp: (String, Int, Intent) -> Unit,
+ closeApp: (String, Int) -> Unit,
+ openPrivacyDashboard: () -> Unit
+ ): PrivacyDialogV2 {
+ return PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+ }
+ }
+
+/**
+ * Controller for [PrivacyDialogV2].
+ *
+ * This controller shows and dismissed the dialog, as well as determining the information to show in
+ * it.
+ */
+@SysUISingleton
+class PrivacyDialogControllerV2(
+ private val permissionManager: PermissionManager,
+ private val packageManager: PackageManager,
+ private val privacyItemController: PrivacyItemController,
+ private val userTracker: UserTracker,
+ private val activityStarter: ActivityStarter,
+ private val backgroundExecutor: Executor,
+ private val uiExecutor: Executor,
+ private val privacyLogger: PrivacyLogger,
+ private val keyguardStateController: KeyguardStateController,
+ private val appOpsController: AppOpsController,
+ private val uiEventLogger: UiEventLogger,
+ private val dialogLaunchAnimator: DialogLaunchAnimator,
+ private val dialogProvider: DialogProvider
+) {
+
+ @Inject
+ constructor(
+ permissionManager: PermissionManager,
+ packageManager: PackageManager,
+ privacyItemController: PrivacyItemController,
+ userTracker: UserTracker,
+ activityStarter: ActivityStarter,
+ @Background backgroundExecutor: Executor,
+ @Main uiExecutor: Executor,
+ privacyLogger: PrivacyLogger,
+ keyguardStateController: KeyguardStateController,
+ appOpsController: AppOpsController,
+ uiEventLogger: UiEventLogger,
+ dialogLaunchAnimator: DialogLaunchAnimator
+ ) : this(
+ permissionManager,
+ packageManager,
+ privacyItemController,
+ userTracker,
+ activityStarter,
+ backgroundExecutor,
+ uiExecutor,
+ privacyLogger,
+ keyguardStateController,
+ appOpsController,
+ uiEventLogger,
+ dialogLaunchAnimator,
+ defaultDialogProvider
+ )
+
+ private var dialog: Dialog? = null
+
+ private val onDialogDismissed =
+ object : PrivacyDialogV2.OnDialogDismissed {
+ override fun onDialogDismissed() {
+ privacyLogger.logPrivacyDialogDismissed()
+ uiEventLogger.log(PrivacyDialogEvent.PRIVACY_DIALOG_DISMISSED)
+ dialog = null
+ }
+ }
+
+ @WorkerThread
+ private fun closeApp(packageName: String, userId: Int) {
+ uiEventLogger.log(
+ PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_CLOSE_APP,
+ userId,
+ packageName
+ )
+ privacyLogger.logCloseAppFromDialog(packageName, userId)
+ ActivityManager.getService().stopAppForUser(packageName, userId)
+ }
+
+ @MainThread
+ private fun manageApp(packageName: String, userId: Int, navigationIntent: Intent) {
+ uiEventLogger.log(
+ PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS,
+ userId,
+ packageName
+ )
+ privacyLogger.logStartSettingsActivityFromDialog(packageName, userId)
+ startActivity(navigationIntent)
+ }
+
+ @MainThread
+ private fun openPrivacyDashboard() {
+ uiEventLogger.log(PrivacyDialogEvent.PRIVACY_DIALOG_CLICK_TO_PRIVACY_DASHBOARD)
+ privacyLogger.logStartPrivacyDashboardFromDialog()
+ startActivity(Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE))
+ }
+
+ @MainThread
+ private fun startActivity(navigationIntent: Intent) {
+ if (!keyguardStateController.isUnlocked) {
+ // If we are locked, hide the dialog so the user can unlock
+ dialog?.hide()
+ }
+ // startActivity calls internally startActivityDismissingKeyguard
+ activityStarter.startActivity(navigationIntent, true) {
+ if (ActivityManager.isStartResultSuccessful(it)) {
+ dismissDialog()
+ } else {
+ dialog?.show()
+ }
+ }
+ }
+
+ @WorkerThread
+ private fun getStartViewPermissionUsageIntent(
+ packageName: String,
+ permGroupName: String,
+ attributionTag: CharSequence?,
+ isAttributionSupported: Boolean
+ ): Intent? {
+ if (attributionTag != null && isAttributionSupported) {
+ val intent = Intent(Intent.ACTION_MANAGE_PERMISSION_USAGE)
+ intent.setPackage(packageName)
+ intent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, permGroupName)
+ intent.putExtra(Intent.EXTRA_ATTRIBUTION_TAGS, arrayOf(attributionTag.toString()))
+ intent.putExtra(Intent.EXTRA_SHOWING_ATTRIBUTION, true)
+ val resolveInfo =
+ packageManager.resolveActivity(intent, PackageManager.ResolveInfoFlags.of(0))
+ if (
+ resolveInfo?.activityInfo?.permission ==
+ Manifest.permission.START_VIEW_PERMISSION_USAGE
+ ) {
+ intent.component = ComponentName(packageName, resolveInfo.activityInfo.name)
+ return intent
+ }
+ }
+ return null
+ }
+
+ fun getDefaultManageAppPermissionsIntent(packageName: String, userId: Int): Intent {
+ val intent = Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS)
+ intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName)
+ intent.putExtra(Intent.EXTRA_USER, UserHandle.of(userId))
+ return intent
+ }
+
+ @WorkerThread
+ private fun permGroupUsage(): List<PermissionGroupUsage> {
+ return permissionManager.getIndicatorAppOpUsageData(appOpsController.isMicMuted)
+ }
+
+ /**
+ * Show the [PrivacyDialogV2]
+ *
+ * This retrieves the permission usage from [PermissionManager] and creates a new
+ * [PrivacyDialogV2] with a list of [PrivacyDialogV2.PrivacyElement] to show.
+ *
+ * This list will be filtered by [filterAndSelect]. Only types available by
+ * [PrivacyItemController] will be shown.
+ *
+ * @param context A context to use to create the dialog.
+ * @see filterAndSelect
+ */
+ fun showDialog(context: Context, view: View? = null) {
+ dismissDialog()
+ backgroundExecutor.execute {
+ val usage = permGroupUsage()
+ val userInfos = userTracker.userProfiles
+ privacyLogger.logUnfilteredPermGroupUsage(usage)
+ val items =
+ usage.mapNotNull {
+ val userInfo =
+ userInfos.firstOrNull { ui -> ui.id == UserHandle.getUserId(it.uid) }
+ if (
+ isAvailable(it.permissionGroupName) && (userInfo != null || it.isPhoneCall)
+ ) {
+ // Only try to get the app name if we actually need it
+ val appName =
+ if (it.isPhoneCall) {
+ ""
+ } else {
+ getLabelForPackage(it.packageName, it.uid)
+ }
+ val userId = UserHandle.getUserId(it.uid)
+ val viewUsageIntent =
+ getStartViewPermissionUsageIntent(
+ it.packageName,
+ it.permissionGroupName,
+ it.attributionTag,
+ // attributionLabel is set only when subattribution policies
+ // are supported and satisfied
+ it.attributionLabel != null
+ )
+ PrivacyDialogV2.PrivacyElement(
+ permGroupToPrivacyType(it.permissionGroupName)!!,
+ it.packageName,
+ userId,
+ appName,
+ it.attributionTag,
+ it.attributionLabel,
+ it.proxyLabel,
+ it.lastAccessTimeMillis,
+ it.isActive,
+ it.isPhoneCall,
+ viewUsageIntent != null,
+ it.permissionGroupName,
+ viewUsageIntent
+ ?: getDefaultManageAppPermissionsIntent(it.packageName, userId)
+ )
+ } else {
+ null
+ }
+ }
+ uiExecutor.execute {
+ val elements = filterAndSelect(items)
+ if (elements.isNotEmpty()) {
+ val d =
+ dialogProvider.makeDialog(
+ context,
+ elements,
+ this::manageApp,
+ this::closeApp,
+ this::openPrivacyDashboard
+ )
+ d.setShowForAllUsers(true)
+ d.addOnDismissListener(onDialogDismissed)
+ if (view != null) {
+ dialogLaunchAnimator.showFromView(d, view)
+ } else {
+ d.show()
+ }
+ privacyLogger.logShowDialogV2Contents(elements)
+ dialog = d
+ } else {
+ privacyLogger.logEmptyDialog()
+ }
+ }
+ }
+ }
+
+ /** Dismisses the dialog */
+ fun dismissDialog() {
+ dialog?.dismiss()
+ }
+
+ @WorkerThread
+ private fun getLabelForPackage(packageName: String, uid: Int): CharSequence {
+ return try {
+ packageManager
+ .getApplicationInfoAsUser(packageName, 0, UserHandle.getUserId(uid))
+ .loadLabel(packageManager)
+ } catch (_: PackageManager.NameNotFoundException) {
+ privacyLogger.logLabelNotFound(packageName)
+ packageName
+ }
+ }
+
+ private fun permGroupToPrivacyType(group: String): PrivacyType? {
+ return when (group) {
+ Manifest.permission_group.CAMERA -> PrivacyType.TYPE_CAMERA
+ Manifest.permission_group.MICROPHONE -> PrivacyType.TYPE_MICROPHONE
+ Manifest.permission_group.LOCATION -> PrivacyType.TYPE_LOCATION
+ else -> null
+ }
+ }
+
+ private fun isAvailable(group: String): Boolean {
+ return when (group) {
+ Manifest.permission_group.CAMERA -> privacyItemController.micCameraAvailable
+ Manifest.permission_group.MICROPHONE -> privacyItemController.micCameraAvailable
+ Manifest.permission_group.LOCATION -> privacyItemController.locationAvailable
+ else -> false
+ }
+ }
+
+ /**
+ * Filters the list of elements to show.
+ *
+ * For each privacy type, it'll return all active elements. If there are no active elements,
+ * it'll return the most recent access
+ */
+ private fun filterAndSelect(
+ list: List<PrivacyDialogV2.PrivacyElement>
+ ): List<PrivacyDialogV2.PrivacyElement> {
+ return list
+ .groupBy { it.type }
+ .toSortedMap()
+ .flatMap { (_, elements) ->
+ val actives = elements.filter { it.isActive }
+ if (actives.isNotEmpty()) {
+ actives.sortedByDescending { it.lastActiveTimestamp }
+ } else {
+ elements.maxByOrNull { it.lastActiveTimestamp }?.let { listOf(it) }
+ ?: emptyList()
+ }
+ }
+ }
+
+ /**
+ * Interface to create a [PrivacyDialogV2].
+ *
+ * Can be used to inject a mock creator.
+ */
+ interface DialogProvider {
+ /** Create a [PrivacyDialogV2]. */
+ fun makeDialog(
+ context: Context,
+ list: List<PrivacyDialogV2.PrivacyElement>,
+ manageApp: (String, Int, Intent) -> Unit,
+ closeApp: (String, Int) -> Unit,
+ openPrivacyDashboard: () -> Unit
+ ): PrivacyDialogV2
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogEvent.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogEvent.kt
index 3ecc5a5e5b00..250976cf47a4 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogEvent.kt
@@ -18,13 +18,20 @@ package com.android.systemui.privacy
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.UiEventLogger.UiEventEnum.RESERVE_NEW_UI_EVENT_ID
enum class PrivacyDialogEvent(private val _id: Int) : UiEventLogger.UiEventEnum {
@UiEvent(doc = "Privacy dialog is clicked by user to go to the app settings page.")
PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS(904),
@UiEvent(doc = "Privacy dialog is dismissed by user.")
- PRIVACY_DIALOG_DISMISSED(905);
+ PRIVACY_DIALOG_DISMISSED(905),
+
+ @UiEvent(doc = "Privacy dialog item is clicked by user to close the app using a sensor.")
+ PRIVACY_DIALOG_ITEM_CLICKED_TO_CLOSE_APP(1396),
+
+ @UiEvent(doc = "Privacy dialog is clicked by user to see the privacy dashboard.")
+ PRIVACY_DIALOG_CLICK_TO_PRIVACY_DASHBOARD(1397);
override fun getId() = _id
-} \ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt
new file mode 100644
index 000000000000..f4aa27d5fcbb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt
@@ -0,0 +1,539 @@
+/*
+ * Copyright (C) 2023 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.privacy
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageItemInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.NameNotFoundException
+import android.content.res.Resources.NotFoundException
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.LayerDrawable
+import android.os.Bundle
+import android.text.TextUtils
+import android.util.Log
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.ColorInt
+import androidx.annotation.DrawableRes
+import androidx.annotation.WorkerThread
+import androidx.core.view.ViewCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import com.android.settingslib.Utils
+import com.android.systemui.R
+import com.android.systemui.animation.ViewHierarchyAnimator
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.maybeForceFullscreen
+import java.lang.ref.WeakReference
+import java.util.concurrent.atomic.AtomicBoolean
+
+/**
+ * Dialog to show ongoing and recent app ops element.
+ *
+ * @param context A context to create the dialog
+ * @param list list of elements to show in the dialog. The elements will show in the same order they
+ * appear in the list
+ * @param manageApp a callback to start an activity for a given package name, user id, and intent
+ * @param closeApp a callback to close an app for a given package name, user id
+ * @param openPrivacyDashboard a callback to open the privacy dashboard
+ * @see PrivacyDialogControllerV2
+ */
+class PrivacyDialogV2(
+ context: Context,
+ private val list: List<PrivacyElement>,
+ private val manageApp: (String, Int, Intent) -> Unit,
+ private val closeApp: (String, Int) -> Unit,
+ private val openPrivacyDashboard: () -> Unit
+) : SystemUIDialog(context, R.style.Theme_PrivacyDialog) {
+
+ private val dismissListeners = mutableListOf<WeakReference<OnDialogDismissed>>()
+ private val dismissed = AtomicBoolean(false)
+ // Note: this will call the dialog create method during init
+ private val decorViewLayoutListener = maybeForceFullscreen()?.component2()
+
+ /**
+ * Add a listener that will be called when the dialog is dismissed.
+ *
+ * If the dialog has already been dismissed, the listener will be called immediately, in the
+ * same thread.
+ */
+ fun addOnDismissListener(listener: OnDialogDismissed) {
+ if (dismissed.get()) {
+ listener.onDialogDismissed()
+ } else {
+ dismissListeners.add(WeakReference(listener))
+ }
+ }
+
+ override fun stop() {
+ dismissed.set(true)
+ val iterator = dismissListeners.iterator()
+ while (iterator.hasNext()) {
+ val el = iterator.next()
+ iterator.remove()
+ el.get()?.onDialogDismissed()
+ }
+ // Remove the layout change listener we may have added to the DecorView.
+ if (decorViewLayoutListener != null) {
+ window!!.decorView.removeOnLayoutChangeListener(decorViewLayoutListener)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ window!!.setGravity(Gravity.CENTER)
+ setTitle(R.string.privacy_dialog_title)
+ setContentView(R.layout.privacy_dialog_v2)
+
+ val closeButton = requireViewById<Button>(R.id.privacy_dialog_close_button)
+ closeButton.setOnClickListener { dismiss() }
+
+ val moreButton = requireViewById<Button>(R.id.privacy_dialog_more_button)
+ moreButton.setOnClickListener { openPrivacyDashboard() }
+
+ val itemsContainer = requireViewById<ViewGroup>(R.id.privacy_dialog_items_container)
+ list.forEach { itemsContainer.addView(createView(it, itemsContainer)) }
+ }
+
+ private fun createView(element: PrivacyElement, itemsContainer: ViewGroup): View {
+ val itemCard =
+ LayoutInflater.from(context)
+ .inflate(R.layout.privacy_dialog_item_v2, itemsContainer, false) as ViewGroup
+
+ updateItemHeader(element, itemCard)
+
+ if (element.isPhoneCall) {
+ return itemCard
+ }
+
+ setItemExpansionBehavior(itemCard)
+
+ configureIndicatorActionButtons(element, itemCard)
+
+ return itemCard
+ }
+
+ private fun updateItemHeader(element: PrivacyElement, itemCard: View) {
+ val itemHeader = itemCard.findViewById<ViewGroup>(R.id.privacy_dialog_item_header)!!
+ val permGroupLabel = context.packageManager.getDefaultPermGroupLabel(element.permGroupName)
+
+ val iconView = itemHeader.findViewById<ImageView>(R.id.privacy_dialog_item_header_icon)!!
+ val indicatorIcon = context.getPermGroupIcon(element.permGroupName)
+ updateIconView(iconView, indicatorIcon, element.isActive)
+ iconView.contentDescription = permGroupLabel
+
+ val titleView = itemHeader.findViewById<TextView>(R.id.privacy_dialog_item_header_title)!!
+ titleView.text = permGroupLabel
+ titleView.contentDescription = permGroupLabel
+
+ val usageText = getUsageText(element)
+ val summaryView =
+ itemHeader.findViewById<TextView>(R.id.privacy_dialog_item_header_summary)!!
+ summaryView.text = usageText
+ summaryView.contentDescription = usageText
+ }
+
+ private fun configureIndicatorActionButtons(element: PrivacyElement, itemCard: View) {
+ val expandedLayout =
+ itemCard.findViewById<ViewGroup>(R.id.privacy_dialog_item_header_expanded_layout)!!
+
+ val buttons: MutableList<View> = mutableListOf()
+ configureCloseAppButton(element, expandedLayout)?.also { buttons.add(it) }
+ buttons.add(configureManageButton(element, expandedLayout))
+
+ val backgroundColor = getBackgroundColor(element.isActive)
+ when (buttons.size) {
+ 0 -> return
+ 1 -> {
+ val background =
+ getMutableDrawable(R.drawable.privacy_dialog_background_large_top_large_bottom)
+ background.setTint(backgroundColor)
+ buttons[0].background = background
+ }
+ else -> {
+ val firstBackground =
+ getMutableDrawable(R.drawable.privacy_dialog_background_large_top_small_bottom)
+ val middleBackground =
+ getMutableDrawable(R.drawable.privacy_dialog_background_small_top_small_bottom)
+ val lastBackground =
+ getMutableDrawable(R.drawable.privacy_dialog_background_small_top_large_bottom)
+ firstBackground.setTint(backgroundColor)
+ middleBackground.setTint(backgroundColor)
+ lastBackground.setTint(backgroundColor)
+ buttons.forEach { it.background = middleBackground }
+ buttons.first().background = firstBackground
+ buttons.last().background = lastBackground
+ }
+ }
+ }
+
+ private fun configureCloseAppButton(element: PrivacyElement, expandedLayout: ViewGroup): View? {
+ if (element.isService || !element.isActive) {
+ return null
+ }
+ val closeAppButton =
+ window.layoutInflater.inflate(
+ R.layout.privacy_dialog_card_button,
+ expandedLayout,
+ false
+ ) as Button
+ expandedLayout.addView(closeAppButton)
+ closeAppButton.id = R.id.privacy_dialog_close_app_button
+ closeAppButton.setText(R.string.privacy_dialog_close_app_button)
+ closeAppButton.setTextColor(getForegroundColor(true))
+ closeAppButton.tag = element
+ closeAppButton.setOnClickListener { v ->
+ v.tag?.let {
+ val element = it as PrivacyElement
+ closeApp(element.packageName, element.userId)
+ closeAppTransition(element.packageName, element.userId)
+ }
+ }
+ return closeAppButton
+ }
+
+ private fun closeAppTransition(packageName: String, userId: Int) {
+ val itemsContainer = requireViewById<ViewGroup>(R.id.privacy_dialog_items_container)
+ var shouldTransition = false
+ for (i in 0 until itemsContainer.getChildCount()) {
+ val itemCard = itemsContainer.getChildAt(i)
+ val button = itemCard.findViewById<Button>(R.id.privacy_dialog_close_app_button)
+ if (button == null || button.tag == null) {
+ continue
+ }
+ val element = button.tag as PrivacyElement
+ if (element.packageName != packageName || element.userId != userId) {
+ continue
+ }
+
+ itemCard.setEnabled(false)
+
+ val expandToggle =
+ itemCard.findViewById<ImageView>(R.id.privacy_dialog_item_header_expand_toggle)!!
+ expandToggle.visibility = View.GONE
+
+ disableIndicatorCardUi(itemCard, element.applicationName)
+
+ val expandedLayout =
+ itemCard.findViewById<View>(R.id.privacy_dialog_item_header_expanded_layout)!!
+ if (expandedLayout.visibility == View.VISIBLE) {
+ expandedLayout.visibility = View.GONE
+ shouldTransition = true
+ }
+ }
+ if (shouldTransition) {
+ ViewHierarchyAnimator.animateNextUpdate(window!!.decorView)
+ }
+ }
+
+ private fun configureManageButton(element: PrivacyElement, expandedLayout: ViewGroup): View {
+ val manageButton =
+ window.layoutInflater.inflate(
+ R.layout.privacy_dialog_card_button,
+ expandedLayout,
+ false
+ ) as Button
+ expandedLayout.addView(manageButton)
+ manageButton.id = R.id.privacy_dialog_manage_app_button
+ manageButton.setText(
+ if (element.isService) R.string.privacy_dialog_manage_service
+ else R.string.privacy_dialog_manage_permissions
+ )
+ manageButton.setTextColor(getForegroundColor(element.isActive))
+ manageButton.tag = element
+ manageButton.setOnClickListener { v ->
+ v.tag?.let {
+ val element = it as PrivacyElement
+ manageApp(element.packageName, element.userId, element.navigationIntent)
+ }
+ }
+ return manageButton
+ }
+
+ private fun disableIndicatorCardUi(itemCard: View, applicationName: CharSequence) {
+ val iconView = itemCard.findViewById<ImageView>(R.id.privacy_dialog_item_header_icon)!!
+ val indicatorIcon = getMutableDrawable(R.drawable.privacy_dialog_check_icon)
+ updateIconView(iconView, indicatorIcon, false)
+
+ val closedAppText =
+ context.getString(R.string.privacy_dialog_close_app_message, applicationName)
+ val summaryView = itemCard.findViewById<TextView>(R.id.privacy_dialog_item_header_summary)!!
+ summaryView.text = closedAppText
+ summaryView.contentDescription = closedAppText
+ }
+
+ private fun setItemExpansionBehavior(itemCard: ViewGroup) {
+ val itemHeader = itemCard.findViewById<ViewGroup>(R.id.privacy_dialog_item_header)!!
+
+ val expandToggle =
+ itemHeader.findViewById<ImageView>(R.id.privacy_dialog_item_header_expand_toggle)!!
+ expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_down)
+ expandToggle.visibility = View.VISIBLE
+
+ ViewCompat.replaceAccessibilityAction(
+ itemCard,
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
+ context.getString(R.string.privacy_dialog_expand_action),
+ null
+ )
+
+ val expandedLayout =
+ itemCard.findViewById<View>(R.id.privacy_dialog_item_header_expanded_layout)!!
+ expandedLayout.setOnClickListener {
+ // Stop clicks from propagating
+ }
+
+ itemCard.setOnClickListener {
+ if (expandedLayout.visibility == View.VISIBLE) {
+ expandedLayout.visibility = View.GONE
+ expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_down)
+ ViewCompat.replaceAccessibilityAction(
+ it!!,
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
+ context.getString(R.string.privacy_dialog_expand_action),
+ null
+ )
+ } else {
+ expandedLayout.visibility = View.VISIBLE
+ expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_up)
+ ViewCompat.replaceAccessibilityAction(
+ it!!,
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
+ context.getString(R.string.privacy_dialog_collapse_action),
+ null
+ )
+ }
+ ViewHierarchyAnimator.animateNextUpdate(
+ rootView = window!!.decorView,
+ excludedViews = setOf(expandedLayout)
+ )
+ }
+ }
+
+ private fun updateIconView(iconView: ImageView, indicatorIcon: Drawable, active: Boolean) {
+ indicatorIcon.setTint(getForegroundColor(active))
+ val backgroundIcon = getMutableDrawable(R.drawable.privacy_dialog_background_circle)
+ backgroundIcon.setTint(getBackgroundColor(active))
+ val backgroundSize =
+ context.resources.getDimension(R.dimen.ongoing_appops_dialog_circle_size).toInt()
+ val indicatorSize =
+ context.resources.getDimension(R.dimen.ongoing_appops_dialog_icon_size).toInt()
+ iconView.setImageDrawable(
+ constructLayeredIcon(indicatorIcon, indicatorSize, backgroundIcon, backgroundSize)
+ )
+ }
+
+ @ColorInt
+ private fun getForegroundColor(active: Boolean) =
+ Utils.getColorAttrDefaultColor(
+ context,
+ if (active) com.android.internal.R.attr.materialColorOnPrimaryFixed
+ else com.android.internal.R.attr.materialColorOnSurface
+ )
+
+ @ColorInt
+ private fun getBackgroundColor(active: Boolean) =
+ Utils.getColorAttrDefaultColor(
+ context,
+ if (active) com.android.internal.R.attr.materialColorPrimaryFixed
+ else com.android.internal.R.attr.materialColorSurfaceContainerHigh
+ )
+
+ private fun getMutableDrawable(@DrawableRes resId: Int) = context.getDrawable(resId)!!.mutate()
+
+ private fun getUsageText(element: PrivacyElement) =
+ if (element.isPhoneCall) {
+ val phoneCallResId =
+ if (element.isActive) R.string.privacy_dialog_active_call_usage
+ else R.string.privacy_dialog_recent_call_usage
+ context.getString(phoneCallResId)
+ } else if (element.attributionLabel == null && element.proxyLabel == null) {
+ val usageResId: Int =
+ if (element.isActive) R.string.privacy_dialog_active_app_usage
+ else R.string.privacy_dialog_recent_app_usage
+ context.getString(usageResId, element.applicationName)
+ } else if (element.attributionLabel == null || element.proxyLabel == null) {
+ val singleUsageResId: Int =
+ if (element.isActive) R.string.privacy_dialog_active_app_usage_1
+ else R.string.privacy_dialog_recent_app_usage_1
+ context.getString(
+ singleUsageResId,
+ element.applicationName,
+ element.attributionLabel ?: element.proxyLabel
+ )
+ } else {
+ val doubleUsageResId: Int =
+ if (element.isActive) R.string.privacy_dialog_active_app_usage_2
+ else R.string.privacy_dialog_recent_app_usage_2
+ context.getString(
+ doubleUsageResId,
+ element.applicationName,
+ element.attributionLabel,
+ element.proxyLabel
+ )
+ }
+
+ companion object {
+ private const val LOG_TAG = "PrivacyDialogV2"
+ private const val REVIEW_PERMISSION_USAGE = "android.intent.action.REVIEW_PERMISSION_USAGE"
+
+ /**
+ * Gets a permission group's icon from the system.
+ *
+ * @param groupName The name of the permission group whose icon we want
+ * @return The permission group's icon, the privacy_dialog_default_permission_icon icon if
+ * the group has no icon, or the group does not exist
+ */
+ @WorkerThread
+ private fun Context.getPermGroupIcon(groupName: String): Drawable {
+ val groupInfo = packageManager.getGroupInfo(groupName)
+ if (groupInfo != null && groupInfo.icon != 0) {
+ val icon = packageManager.loadDrawable(groupInfo.packageName, groupInfo.icon)
+ if (icon != null) {
+ return icon
+ }
+ }
+
+ return getDrawable(R.drawable.privacy_dialog_default_permission_icon)!!.mutate()
+ }
+
+ /**
+ * Gets a permission group's label from the system.
+ *
+ * @param groupName The name of the permission group whose label we want
+ * @return The permission group's label, or the group name, if the group is invalid
+ */
+ @WorkerThread
+ private fun PackageManager.getDefaultPermGroupLabel(groupName: String): CharSequence {
+ val groupInfo = getGroupInfo(groupName) ?: return groupName
+ return groupInfo.loadSafeLabel(
+ this,
+ 0f,
+ TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
+ )
+ }
+
+ /**
+ * Get the [infos][PackageItemInfo] for the given permission group.
+ *
+ * @param groupName the group
+ * @return The info of permission group or null if the group does not have runtime
+ * permissions.
+ */
+ @WorkerThread
+ private fun PackageManager.getGroupInfo(groupName: String): PackageItemInfo? {
+ try {
+ return getPermissionGroupInfo(groupName, 0)
+ } catch (e: NameNotFoundException) {
+ /* ignore */
+ }
+ try {
+ return getPermissionInfo(groupName, 0)
+ } catch (e: NameNotFoundException) {
+ /* ignore */
+ }
+ return null
+ }
+
+ @WorkerThread
+ private fun PackageManager.loadDrawable(pkg: String, @DrawableRes resId: Int): Drawable? {
+ return try {
+ getResourcesForApplication(pkg).getDrawable(resId, null)?.mutate()
+ } catch (e: NotFoundException) {
+ Log.w(LOG_TAG, "Couldn't get resource", e)
+ null
+ } catch (e: NameNotFoundException) {
+ Log.w(LOG_TAG, "Couldn't get resource", e)
+ null
+ }
+ }
+
+ private fun constructLayeredIcon(
+ icon: Drawable,
+ iconSize: Int,
+ background: Drawable,
+ backgroundSize: Int
+ ): Drawable {
+ val layered = LayerDrawable(arrayOf(background, icon))
+ layered.setLayerSize(0, backgroundSize, backgroundSize)
+ layered.setLayerGravity(0, Gravity.CENTER)
+ layered.setLayerSize(1, iconSize, iconSize)
+ layered.setLayerGravity(1, Gravity.CENTER)
+ return layered
+ }
+ }
+
+ /** */
+ data class PrivacyElement(
+ val type: PrivacyType,
+ val packageName: String,
+ val userId: Int,
+ val applicationName: CharSequence,
+ val attributionTag: CharSequence?,
+ val attributionLabel: CharSequence?,
+ val proxyLabel: CharSequence?,
+ val lastActiveTimestamp: Long,
+ val isActive: Boolean,
+ val isPhoneCall: Boolean,
+ val isService: Boolean,
+ val permGroupName: String,
+ val navigationIntent: Intent
+ ) {
+ private val builder = StringBuilder("PrivacyElement(")
+
+ init {
+ builder.append("type=${type.logName}")
+ builder.append(", packageName=$packageName")
+ builder.append(", userId=$userId")
+ builder.append(", appName=$applicationName")
+ if (attributionTag != null) {
+ builder.append(", attributionTag=$attributionTag")
+ }
+ if (attributionLabel != null) {
+ builder.append(", attributionLabel=$attributionLabel")
+ }
+ if (proxyLabel != null) {
+ builder.append(", proxyLabel=$proxyLabel")
+ }
+ builder.append(", lastActive=$lastActiveTimestamp")
+ if (isActive) {
+ builder.append(", active")
+ }
+ if (isPhoneCall) {
+ builder.append(", phoneCall")
+ }
+ if (isService) {
+ builder.append(", service")
+ }
+ builder.append(", permGroupName=$permGroupName")
+ builder.append(", navigationIntent=$navigationIntent)")
+ }
+
+ override fun toString(): String = builder.toString()
+ }
+
+ /** */
+ interface OnDialogDismissed {
+ fun onDialogDismissed()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
index f934346d9775..1a4642f4df74 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
@@ -18,11 +18,12 @@ package com.android.systemui.privacy.logging
import android.icu.text.SimpleDateFormat
import android.permission.PermissionGroupUsage
-import com.android.systemui.log.dagger.PrivacyLog
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel
import com.android.systemui.log.core.LogMessage
+import com.android.systemui.log.dagger.PrivacyLog
import com.android.systemui.privacy.PrivacyDialog
+import com.android.systemui.privacy.PrivacyDialogV2
import com.android.systemui.privacy.PrivacyItem
import java.util.Locale
import javax.inject.Inject
@@ -126,6 +127,14 @@ class PrivacyLogger @Inject constructor(
})
}
+ fun logShowDialogV2Contents(contents: List<PrivacyDialogV2.PrivacyElement>) {
+ log(LogLevel.INFO, {
+ str1 = contents.toString()
+ }, {
+ "Privacy dialog shown. Contents: $str1"
+ })
+ }
+
fun logEmptyDialog() {
log(LogLevel.WARNING, {}, {
"Trying to show an empty dialog"
@@ -147,6 +156,23 @@ class PrivacyLogger @Inject constructor(
})
}
+ fun logCloseAppFromDialog(packageName: String, userId: Int) {
+ log(LogLevel.INFO, {
+ str1 = packageName
+ int1 = userId
+ }, {
+ "Close app from dialog for packageName=$str1, userId=$int1"
+ })
+ }
+
+ fun logStartPrivacyDashboardFromDialog() {
+ log(LogLevel.INFO, {}, { "Start privacy dashboard from dialog" })
+ }
+
+ fun logLabelNotFound(packageName: String) {
+ log(LogLevel.WARNING, { str1 = packageName }, { "Label not found for: $str1" })
+ }
+
private fun listToString(list: List<PrivacyItem>): String {
return list.joinToString(separator = ", ", transform = PrivacyItem::log)
}
@@ -158,4 +184,4 @@ class PrivacyLogger @Inject constructor(
) {
buffer.log(TAG, logLevel, initializer, printer)
}
-} \ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt b/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt
index 33c47cc082e1..0941a2082cfd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt
@@ -14,10 +14,13 @@ import com.android.internal.logging.UiEventLogger
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.appops.AppOpsController
import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyChipEvent
import com.android.systemui.privacy.PrivacyDialogController
+import com.android.systemui.privacy.PrivacyDialogControllerV2
import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.privacy.PrivacyItemController
import com.android.systemui.privacy.logging.PrivacyLogger
@@ -49,6 +52,7 @@ class HeaderPrivacyIconsController @Inject constructor(
private val uiEventLogger: UiEventLogger,
@Named(SHADE_HEADER) private val privacyChip: OngoingPrivacyChip,
private val privacyDialogController: PrivacyDialogController,
+ private val privacyDialogControllerV2: PrivacyDialogControllerV2,
private val privacyLogger: PrivacyLogger,
@Named(SHADE_HEADER) private val iconContainer: StatusIconContainer,
private val permissionManager: PermissionManager,
@@ -58,7 +62,8 @@ class HeaderPrivacyIconsController @Inject constructor(
private val appOpsController: AppOpsController,
private val broadcastDispatcher: BroadcastDispatcher,
private val safetyCenterManager: SafetyCenterManager,
- private val deviceProvisionedController: DeviceProvisionedController
+ private val deviceProvisionedController: DeviceProvisionedController,
+ private val featureFlags: FeatureFlags
) {
var chipVisibilityListener: ChipVisibilityListener? = null
@@ -143,7 +148,11 @@ class HeaderPrivacyIconsController @Inject constructor(
// If the privacy chip is visible, it means there were some indicators
uiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK)
if (safetyCenterEnabled) {
- showSafetyCenter()
+ if (featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)) {
+ privacyDialogControllerV2.showDialog(privacyChip.context, privacyChip)
+ } else {
+ showSafetyCenter()
+ }
} else {
privacyDialogController.showDialog(privacyChip.context)
}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index a737a8bca585..bf40a2d0ad51 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -84,6 +84,8 @@ import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
@@ -94,6 +96,8 @@ import com.android.systemui.navigationbar.NavigationBarView;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.navigationbar.buttons.KeyButtonView;
import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener;
+import com.android.systemui.scene.domain.interactor.SceneInteractor;
+import com.android.systemui.scene.shared.model.SceneContainerNames;
import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.ShadeViewController;
@@ -120,6 +124,7 @@ import java.util.concurrent.Executor;
import java.util.function.Supplier;
import javax.inject.Inject;
+import javax.inject.Provider;
/**
* Class to send information from overview to launcher with a binder.
@@ -139,6 +144,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
private static final long MAX_BACKOFF_MILLIS = 10 * 60 * 1000;
private final Context mContext;
+ private final FeatureFlags mFeatureFlags;
private final Executor mMainExecutor;
private final ShellInterface mShellInterface;
private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
@@ -147,6 +153,8 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
private final Handler mHandler;
private final Lazy<NavigationBarController> mNavBarControllerLazy;
private final NotificationShadeWindowController mStatusBarWinController;
+ private final Provider<SceneInteractor> mSceneInteractor;
+
private final Runnable mConnectionRunnable = () ->
internalConnectToCurrentUser("runnable: startConnectionToCurrentUser");
private final ComponentName mRecentsComponentName;
@@ -209,17 +217,28 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
mInputFocusTransferStarted = true;
mInputFocusTransferStartY = event.getY();
mInputFocusTransferStartMillis = event.getEventTime();
- centralSurfaces.onInputFocusTransfer(
- mInputFocusTransferStarted, false /* cancel */,
- 0 /* velocity */);
+
+ // If scene framework is enabled, set the scene container window to
+ // visible and let the touch "slip" into that window.
+ if (mFeatureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
+ mSceneInteractor.get().setVisible(
+ SceneContainerNames.SYSTEM_UI_DEFAULT, true);
+ } else {
+ centralSurfaces.onInputFocusTransfer(
+ mInputFocusTransferStarted, false /* cancel */,
+ 0 /* velocity */);
+ }
}
if (action == ACTION_UP || action == ACTION_CANCEL) {
mInputFocusTransferStarted = false;
- float velocity = (event.getY() - mInputFocusTransferStartY)
- / (event.getEventTime() - mInputFocusTransferStartMillis);
- centralSurfaces.onInputFocusTransfer(mInputFocusTransferStarted,
- action == ACTION_CANCEL,
- velocity);
+
+ if (!mFeatureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
+ float velocity = (event.getY() - mInputFocusTransferStartY)
+ / (event.getEventTime() - mInputFocusTransferStartMillis);
+ centralSurfaces.onInputFocusTransfer(mInputFocusTransferStarted,
+ action == ACTION_CANCEL,
+ velocity);
+ }
}
event.recycle();
});
@@ -552,6 +571,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
NavigationModeController navModeController,
NotificationShadeWindowController statusBarWinController,
SysUiState sysUiState,
+ Provider<SceneInteractor> sceneInteractor,
UserTracker userTracker,
ScreenLifecycle screenLifecycle,
WakefulnessLifecycle wakefulnessLifecycle,
@@ -559,6 +579,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
DisplayTracker displayTracker,
KeyguardUnlockAnimationController sysuiUnlockAnimationController,
AssistUtils assistUtils,
+ FeatureFlags featureFlags,
DumpManager dumpManager,
Optional<UnfoldTransitionProgressForwarder> unfoldTransitionProgressForwarder
) {
@@ -568,6 +589,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
}
mContext = context;
+ mFeatureFlags = featureFlags;
mMainExecutor = mainExecutor;
mShellInterface = shellInterface;
mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
@@ -575,6 +597,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
mHandler = new Handler();
mNavBarControllerLazy = navBarControllerLazy;
mStatusBarWinController = statusBarWinController;
+ mSceneInteractor = sceneInteractor;
mUserTracker = userTracker;
mConnectionBackoffAttempts = 0;
mRecentsComponentName = ComponentName.unflattenFromString(context.getString(
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index 4582370679ab..f03f040c206d 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -18,11 +18,14 @@ package com.android.systemui.scene.domain.interactor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.scene.data.repository.SceneContainerRepository
+import com.android.systemui.scene.shared.model.RemoteUserInput
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.scene.shared.model.SceneTransitionModel
import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
/**
* Generic business logic and app state accessors for the scene framework.
@@ -92,4 +95,14 @@ constructor(
fun sceneTransitions(containerName: String): StateFlow<SceneTransitionModel?> {
return repository.sceneTransitions(containerName)
}
+
+ private val _remoteUserInput: MutableStateFlow<RemoteUserInput?> = MutableStateFlow(null)
+
+ /** A flow of motion events originating from outside of the scene framework. */
+ val remoteUserInput: StateFlow<RemoteUserInput?> = _remoteUserInput.asStateFlow()
+
+ /** Handles a remote user input. */
+ fun onRemoteUserInput(input: RemoteUserInput) {
+ _remoteUserInput.value = input
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/RemoteUserInput.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/RemoteUserInput.kt
new file mode 100644
index 000000000000..680de590a3fc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/RemoteUserInput.kt
@@ -0,0 +1,35 @@
+package com.android.systemui.scene.shared.model
+
+import android.view.MotionEvent
+
+/** A representation of user input that is used by the scene framework. */
+data class RemoteUserInput(
+ val x: Float,
+ val y: Float,
+ val action: RemoteUserInputAction,
+) {
+ companion object {
+ fun translateMotionEvent(event: MotionEvent): RemoteUserInput {
+ return RemoteUserInput(
+ x = event.x,
+ y = event.y,
+ action =
+ when (event.actionMasked) {
+ MotionEvent.ACTION_DOWN -> RemoteUserInputAction.DOWN
+ MotionEvent.ACTION_MOVE -> RemoteUserInputAction.MOVE
+ MotionEvent.ACTION_UP -> RemoteUserInputAction.UP
+ MotionEvent.ACTION_CANCEL -> RemoteUserInputAction.CANCEL
+ else -> RemoteUserInputAction.UNKNOWN
+ }
+ )
+ }
+ }
+}
+
+enum class RemoteUserInputAction {
+ DOWN,
+ MOVE,
+ UP,
+ CANCEL,
+ UNKNOWN,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
index c456be6e5ab2..b89179289a3d 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
@@ -2,6 +2,7 @@ package com.android.systemui.scene.ui.view
import android.content.Context
import android.util.AttributeSet
+import android.view.MotionEvent
import android.view.View
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneContainerConfig
@@ -16,11 +17,15 @@ class SceneWindowRootView(
context,
attrs,
) {
+
+ private lateinit var viewModel: SceneContainerViewModel
+
fun init(
viewModel: SceneContainerViewModel,
containerConfig: SceneContainerConfig,
scenes: Set<Scene>,
) {
+ this.viewModel = viewModel
SceneWindowRootViewBinder.bind(
view = this@SceneWindowRootView,
viewModel = viewModel,
@@ -32,6 +37,14 @@ class SceneWindowRootView(
)
}
+ override fun onTouchEvent(event: MotionEvent?): Boolean {
+ return event?.let {
+ viewModel.onRemoteUserInput(event)
+ true
+ }
+ ?: false
+ }
+
override fun setVisibility(visibility: Int) {
// Do nothing. We don't want external callers to invoke this. Instead, we drive our own
// visibility from our view-binder.
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index 8c1ad9b4571b..005f48d9f250 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -16,7 +16,9 @@
package com.android.systemui.scene.ui.viewmodel
+import android.view.MotionEvent
import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.RemoteUserInput
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import kotlinx.coroutines.flow.StateFlow
@@ -26,6 +28,9 @@ class SceneContainerViewModel(
private val interactor: SceneInteractor,
val containerName: String,
) {
+ /** A flow of motion events originating from outside of the scene framework. */
+ val remoteUserInput: StateFlow<RemoteUserInput?> = interactor.remoteUserInput
+
/**
* Keys of all scenes in the container.
*
@@ -49,4 +54,9 @@ class SceneContainerViewModel(
fun setSceneTransitionProgress(progress: Float) {
interactor.setSceneTransitionProgress(containerName, progress)
}
+
+ /** Handles a [MotionEvent] representing remote user input. */
+ fun onRemoteUserInput(event: MotionEvent) {
+ interactor.onRemoteUserInput(RemoteUserInput.translateMotionEvent(event))
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index e428976c5ce2..ed7cbffc880b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -28,6 +28,7 @@ import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
import static com.android.systemui.classifier.Classifier.GENERIC;
import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
import static com.android.systemui.classifier.Classifier.UNLOCK;
+import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadScroll;
import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadThreeFingerSwipe;
import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
@@ -70,6 +71,7 @@ import android.provider.Settings;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.MathUtils;
+import android.view.HapticFeedbackConstants;
import android.view.InputDevice;
import android.view.LayoutInflater;
import android.view.MotionEvent;
@@ -2632,12 +2634,16 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
}
if (!mStatusBarStateController.isDozing()) {
- mVibratorHelper.vibrate(
- Process.myUid(),
- mView.getContext().getPackageName(),
- ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT,
- "falsing-additional-tap-required",
- TOUCH_VIBRATION_ATTRIBUTES);
+ if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+ mVibratorHelper.performHapticFeedback(mView, HapticFeedbackConstants.REJECT);
+ } else {
+ mVibratorHelper.vibrate(
+ Process.myUid(),
+ mView.getContext().getPackageName(),
+ ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT,
+ "falsing-additional-tap-required",
+ TOUCH_VIBRATION_ATTRIBUTES);
+ }
}
}
@@ -3504,7 +3510,14 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
private void maybeVibrateOnOpening(boolean openingWithTouch) {
if (mVibrateOnOpening && mBarState != KEYGUARD && mBarState != SHADE_LOCKED) {
if (!openingWithTouch || !mHasVibratedOnOpen) {
- mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
+ if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+ mVibratorHelper.performHapticFeedback(
+ mView,
+ HapticFeedbackConstants.GESTURE_START
+ );
+ } else {
+ mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
+ }
mHasVibratedOnOpen = true;
mShadeLog.v("Vibrating on opening, mHasVibratedOnOpen=true");
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 92df78bac17f..6304c1ea2635 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -168,7 +168,10 @@ public class CommandQueue extends IStatusBar.Stub implements
private static final int MSG_ENTER_STAGE_SPLIT_FROM_RUNNING_APP = 71 << MSG_SHIFT;
private static final int MSG_SHOW_MEDIA_OUTPUT_SWITCHER = 72 << MSG_SHIFT;
private static final int MSG_TOGGLE_TASKBAR = 73 << MSG_SHIFT;
-
+ private static final int MSG_SETTING_CHANGED = 74 << MSG_SHIFT;
+ private static final int MSG_LOCK_TASK_MODE_CHANGED = 75 << MSG_SHIFT;
+ private static final int MSG_CONFIRM_IMMERSIVE_PROMPT = 77 << MSG_SHIFT;
+ private static final int MSG_IMMERSIVE_CHANGED = 78 << MSG_SHIFT;
public static final int FLAG_EXCLUDE_NONE = 0;
public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0;
public static final int FLAG_EXCLUDE_RECENTS_PANEL = 1 << 1;
@@ -498,6 +501,16 @@ public class CommandQueue extends IStatusBar.Stub implements
* @see IStatusBar#showMediaOutputSwitcher
*/
default void showMediaOutputSwitcher(String packageName) {}
+
+ /**
+ * @see IStatusBar#confirmImmersivePrompt
+ */
+ default void confirmImmersivePrompt() {}
+
+ /**
+ * @see IStatusBar#immersiveModeChanged
+ */
+ default void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {}
}
@VisibleForTesting
@@ -783,6 +796,23 @@ public class CommandQueue extends IStatusBar.Stub implements
}
@Override
+ public void confirmImmersivePrompt() {
+ synchronized (mLock) {
+ mHandler.obtainMessage(MSG_CONFIRM_IMMERSIVE_PROMPT).sendToTarget();
+ }
+ }
+
+ @Override
+ public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {
+ synchronized (mLock) {
+ final SomeArgs args = SomeArgs.obtain();
+ args.argi1 = rootDisplayAreaId;
+ args.argi2 = isImmersiveMode ? 1 : 0;
+ mHandler.obtainMessage(MSG_IMMERSIVE_CHANGED, args).sendToTarget();
+ }
+ }
+
+ @Override
public void appTransitionPending(int displayId) {
appTransitionPending(displayId, false /* forced */);
}
@@ -1810,6 +1840,19 @@ public class CommandQueue extends IStatusBar.Stub implements
mCallbacks.get(i).showMediaOutputSwitcher(clientPackageName);
}
break;
+ case MSG_CONFIRM_IMMERSIVE_PROMPT:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).confirmImmersivePrompt();
+ }
+ break;
+ case MSG_IMMERSIVE_CHANGED:
+ args = (SomeArgs) msg.obj;
+ int rootDisplayAreaId = args.argi1;
+ boolean isImmersiveMode = args.argi2 != 0;
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).immersiveModeChanged(rootDisplayAreaId, isImmersiveMode);
+ }
+ break;
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
new file mode 100644
index 000000000000..a7ec02ff43c3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
@@ -0,0 +1,590 @@
+/*
+ * Copyright (C) 2023 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 android.app.ActivityManager.LOCK_TASK_MODE_LOCKED;
+import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
+import static android.app.StatusBarManager.DISABLE_BACK;
+import static android.app.StatusBarManager.DISABLE_HOME;
+import static android.app.StatusBarManager.DISABLE_RECENT;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.ViewRootImpl.CLIENT_IMMERSIVE_CONFIRMATION;
+import static android.view.ViewRootImpl.CLIENT_TRANSIENT;
+import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
+import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID;
+
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.database.ContentObserver;
+import android.graphics.Insets;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.service.vr.IVrManager;
+import android.service.vr.IVrStateCallbacks;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
+import android.view.WindowInsets.Type;
+import android.view.WindowManager;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.systemui.CoreStartable;
+import com.android.systemui.R;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
+import com.android.systemui.util.settings.SecureSettings;
+
+import javax.inject.Inject;
+
+/**
+ * Helper to manage showing/hiding a confirmation prompt when the navigation bar is hidden
+ * entering immersive mode.
+ */
+public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Callbacks,
+ TaskStackChangeListener {
+ private static final String TAG = "ImmersiveModeConfirm";
+ private static final boolean DEBUG = false;
+ private static final boolean DEBUG_SHOW_EVERY_TIME = false; // super annoying, use with caution
+ private static final String CONFIRMED = "confirmed";
+ private static final int IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE =
+ WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL;
+
+ private static boolean sConfirmed;
+ private final SecureSettings mSecureSettings;
+
+ private Context mDisplayContext;
+ private final Context mSysUiContext;
+ private final Handler mHandler = new H(Looper.getMainLooper());
+ private long mShowDelayMs = 0L;
+ private final IBinder mWindowToken = new Binder();
+ private final CommandQueue mCommandQueue;
+
+ private ClingWindowView mClingWindow;
+ /** The last {@link WindowManager} that is used to add the confirmation window. */
+ @Nullable
+ private WindowManager mWindowManager;
+ /**
+ * The WindowContext that is registered with {@link #mWindowManager} with options to specify the
+ * {@link RootDisplayArea} to attach the confirmation window.
+ */
+ @Nullable
+ private Context mWindowContext;
+ /**
+ * The root display area feature id that the {@link #mWindowContext} is attaching to.
+ */
+ private int mWindowContextRootDisplayAreaId = FEATURE_UNDEFINED;
+ // Local copy of vr mode enabled state, to avoid calling into VrManager with
+ // the lock held.
+ private boolean mVrModeEnabled = false;
+ private boolean mCanSystemBarsBeShownByUser = true;
+ private int mLockTaskState = LOCK_TASK_MODE_NONE;
+ private boolean mNavBarEmpty;
+
+ private ContentObserver mContentObserver;
+
+ @Inject
+ public ImmersiveModeConfirmation(Context context, CommandQueue commandQueue,
+ SecureSettings secureSettings) {
+ mSysUiContext = context;
+ final Display display = mSysUiContext.getDisplay();
+ mDisplayContext = display.getDisplayId() == DEFAULT_DISPLAY
+ ? mSysUiContext : mSysUiContext.createDisplayContext(display);
+ mCommandQueue = commandQueue;
+ mSecureSettings = secureSettings;
+ }
+
+ boolean loadSetting(int currentUserId) {
+ final boolean wasConfirmed = sConfirmed;
+ sConfirmed = false;
+ if (DEBUG) Log.d(TAG, String.format("loadSetting() currentUserId=%d", currentUserId));
+ String value = null;
+ try {
+ value = mSecureSettings.getStringForUser(Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS,
+ UserHandle.USER_CURRENT);
+ sConfirmed = CONFIRMED.equals(value);
+ if (DEBUG) Log.d(TAG, "Loaded sConfirmed=" + sConfirmed);
+ } catch (Throwable t) {
+ Log.w(TAG, "Error loading confirmations, value=" + value, t);
+ }
+ return sConfirmed != wasConfirmed;
+ }
+
+ private static void saveSetting(Context context) {
+ if (DEBUG) Log.d(TAG, "saveSetting()");
+ try {
+ final String value = sConfirmed ? CONFIRMED : null;
+ Settings.Secure.putStringForUser(context.getContentResolver(),
+ Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS,
+ value,
+ UserHandle.USER_CURRENT);
+ if (DEBUG) Log.d(TAG, "Saved value=" + value);
+ } catch (Throwable t) {
+ Log.w(TAG, "Error saving confirmations, sConfirmed=" + sConfirmed, t);
+ }
+ }
+
+ @Override
+ public void onDisplayRemoved(int displayId) {
+ if (displayId != mSysUiContext.getDisplayId()) {
+ return;
+ }
+ mHandler.removeMessages(H.SHOW);
+ mHandler.removeMessages(H.HIDE);
+ IVrManager vrManager = IVrManager.Stub.asInterface(
+ ServiceManager.getService(Context.VR_SERVICE));
+ if (vrManager != null) {
+ try {
+ vrManager.unregisterListener(mVrStateCallbacks);
+ } catch (RemoteException ex) {
+ }
+ }
+ mCommandQueue.removeCallback(this);
+ }
+
+ private void onSettingChanged(int currentUserId) {
+ final boolean changed = loadSetting(currentUserId);
+ // Remove the window if the setting changes to be confirmed.
+ if (changed && sConfirmed) {
+ mHandler.sendEmptyMessage(H.HIDE);
+ }
+ }
+
+ @Override
+ public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {
+ mHandler.removeMessages(H.SHOW);
+ if (isImmersiveMode) {
+ if (DEBUG) Log.d(TAG, "immersiveModeChanged() sConfirmed=" + sConfirmed);
+ boolean userSetupComplete = (mSecureSettings.getIntForUser(
+ Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) != 0);
+
+ if ((DEBUG_SHOW_EVERY_TIME || !sConfirmed)
+ && userSetupComplete
+ && !mVrModeEnabled
+ && mCanSystemBarsBeShownByUser
+ && !mNavBarEmpty
+ && !UserManager.isDeviceInDemoMode(mDisplayContext)
+ && (mLockTaskState != LOCK_TASK_MODE_LOCKED)) {
+ final Message msg = mHandler.obtainMessage(
+ H.SHOW);
+ msg.arg1 = rootDisplayAreaId;
+ mHandler.sendMessageDelayed(msg, mShowDelayMs);
+ }
+ } else {
+ mHandler.sendEmptyMessage(H.HIDE);
+ }
+ }
+
+ @Override
+ public void disable(int displayId, int disableFlag, int disableFlag2, boolean animate) {
+ if (mSysUiContext.getDisplayId() != displayId) {
+ return;
+ }
+ final int disableNavigationBar = (DISABLE_HOME | DISABLE_BACK | DISABLE_RECENT);
+ mNavBarEmpty = (disableFlag & disableNavigationBar) == disableNavigationBar;
+ }
+
+ @Override
+ public void confirmImmersivePrompt() {
+ if (mClingWindow != null) {
+ if (DEBUG) Log.d(TAG, "confirmImmersivePrompt()");
+ mHandler.post(mConfirm);
+ }
+ }
+
+ private void handleHide() {
+ if (mClingWindow != null) {
+ if (DEBUG) Log.d(TAG, "Hiding immersive mode confirmation");
+ if (mWindowManager != null) {
+ try {
+ mWindowManager.removeView(mClingWindow);
+ } catch (WindowManager.InvalidDisplayException e) {
+ Log.w(TAG, "Fail to hide the immersive confirmation window because of "
+ + e);
+ }
+ mWindowManager = null;
+ mWindowContext = null;
+ }
+ mClingWindow = null;
+ }
+ }
+
+ private WindowManager.LayoutParams getClingWindowLayoutParams() {
+ final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE,
+ WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
+ PixelFormat.TRANSLUCENT);
+ lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~Type.statusBars());
+ // Trusted overlay so touches outside the touchable area are allowed to pass through
+ lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
+ | WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+ | WindowManager.LayoutParams.PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW;
+ lp.setTitle("ImmersiveModeConfirmation");
+ lp.windowAnimations = com.android.internal.R.style.Animation_ImmersiveModeConfirmation;
+ lp.token = getWindowToken();
+ return lp;
+ }
+
+ private FrameLayout.LayoutParams getBubbleLayoutParams() {
+ return new FrameLayout.LayoutParams(
+ mSysUiContext.getResources().getDimensionPixelSize(
+ R.dimen.immersive_mode_cling_width),
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ Gravity.CENTER_HORIZONTAL | Gravity.TOP);
+ }
+
+ /**
+ * @return the window token that's used by all ImmersiveModeConfirmation windows.
+ */
+ IBinder getWindowToken() {
+ return mWindowToken;
+ }
+
+ @Override
+ public void start() {
+ if (CLIENT_TRANSIENT || CLIENT_IMMERSIVE_CONFIRMATION) {
+ mCommandQueue.addCallback(this);
+
+ final Resources r = mSysUiContext.getResources();
+ mShowDelayMs = r.getInteger(R.integer.dock_enter_exit_duration) * 3L;
+ mCanSystemBarsBeShownByUser = !r.getBoolean(
+ R.bool.config_remoteInsetsControllerControlsSystemBars) || r.getBoolean(
+ R.bool.config_remoteInsetsControllerSystemBarsCanBeShownByUserAction);
+ IVrManager vrManager = IVrManager.Stub.asInterface(
+ ServiceManager.getService(Context.VR_SERVICE));
+ if (vrManager != null) {
+ try {
+ mVrModeEnabled = vrManager.getVrModeState();
+ vrManager.registerListener(mVrStateCallbacks);
+ mVrStateCallbacks.onVrStateChanged(mVrModeEnabled);
+ } catch (RemoteException e) {
+ // Ignore, we cannot do anything if we failed to access vr manager.
+ }
+ }
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(this);
+ mContentObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ onSettingChanged(mSysUiContext.getUserId());
+ }
+ };
+
+ // Register to listen for changes in Settings.Secure settings.
+ mSecureSettings.registerContentObserverForUser(
+ Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS, mContentObserver,
+ UserHandle.USER_CURRENT);
+ mSecureSettings.registerContentObserverForUser(
+ Settings.Secure.USER_SETUP_COMPLETE, mContentObserver,
+ UserHandle.USER_CURRENT);
+ }
+ }
+
+ private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() {
+ @Override
+ public void onVrStateChanged(boolean enabled) {
+ mVrModeEnabled = enabled;
+ if (mVrModeEnabled) {
+ mHandler.removeMessages(H.SHOW);
+ mHandler.sendEmptyMessage(H.HIDE);
+ }
+ }
+ };
+
+ private class ClingWindowView extends FrameLayout {
+ private static final int BGCOLOR = 0x80000000;
+ private static final int OFFSET_DP = 96;
+ private static final int ANIMATION_DURATION = 250;
+
+ private final Runnable mConfirm;
+ private final ColorDrawable mColor = new ColorDrawable(0);
+ private final Interpolator mInterpolator;
+ private ValueAnimator mColorAnim;
+ private ViewGroup mClingLayout;
+
+ private Runnable mUpdateLayoutRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (mClingLayout != null && mClingLayout.getParent() != null) {
+ mClingLayout.setLayoutParams(getBubbleLayoutParams());
+ }
+ }
+ };
+
+ private ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener =
+ new ViewTreeObserver.OnComputeInternalInsetsListener() {
+ private final int[] mTmpInt2 = new int[2];
+
+ @Override
+ public void onComputeInternalInsets(
+ ViewTreeObserver.InternalInsetsInfo inoutInfo) {
+ // Set touchable region to cover the cling layout.
+ mClingLayout.getLocationInWindow(mTmpInt2);
+ inoutInfo.setTouchableInsets(
+ ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+ inoutInfo.touchableRegion.set(
+ mTmpInt2[0],
+ mTmpInt2[1],
+ mTmpInt2[0] + mClingLayout.getWidth(),
+ mTmpInt2[1] + mClingLayout.getHeight());
+ }
+ };
+
+ private BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
+ post(mUpdateLayoutRunnable);
+ }
+ }
+ };
+
+ ClingWindowView(Context context, Runnable confirm) {
+ super(context);
+ mConfirm = confirm;
+ setBackground(mColor);
+ setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
+ mInterpolator = AnimationUtils
+ .loadInterpolator(mContext, android.R.interpolator.linear_out_slow_in);
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ DisplayMetrics metrics = new DisplayMetrics();
+ mContext.getDisplay().getMetrics(metrics);
+ float density = metrics.density;
+
+ getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsListener);
+
+ // create the confirmation cling
+ mClingLayout = (ViewGroup)
+ View.inflate(mSysUiContext, R.layout.immersive_mode_cling, null);
+
+ TypedArray ta = mDisplayContext.obtainStyledAttributes(
+ new int[]{android.R.attr.colorAccent});
+ int colorAccent = ta.getColor(0, 0);
+ ta.recycle();
+ mClingLayout.setBackgroundColor(colorAccent);
+ ImageView expandMore = mClingLayout.findViewById(R.id.immersive_cling_ic_expand_more);
+ if (expandMore != null) {
+ expandMore.setImageTintList(ColorStateList.valueOf(colorAccent));
+ }
+ ImageView lightBgCirc = mClingLayout.findViewById(R.id.immersive_cling_back_bg_light);
+ if (lightBgCirc != null) {
+ // Set transparency to 50%
+ lightBgCirc.setImageAlpha(128);
+ }
+
+ final Button ok = mClingLayout.findViewById(R.id.ok);
+ ok.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mConfirm.run();
+ }
+ });
+ addView(mClingLayout, getBubbleLayoutParams());
+
+ if (ActivityManager.isHighEndGfx()) {
+ final View cling = mClingLayout;
+ cling.setAlpha(0f);
+ cling.setTranslationY(-OFFSET_DP * density);
+
+ postOnAnimation(new Runnable() {
+ @Override
+ public void run() {
+ cling.animate()
+ .alpha(1f)
+ .translationY(0)
+ .setDuration(ANIMATION_DURATION)
+ .setInterpolator(mInterpolator)
+ .withLayer()
+ .start();
+
+ mColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), 0, BGCOLOR);
+ mColorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ final int c = (Integer) animation.getAnimatedValue();
+ mColor.setColor(c);
+ }
+ });
+ mColorAnim.setDuration(ANIMATION_DURATION);
+ mColorAnim.setInterpolator(mInterpolator);
+ mColorAnim.start();
+ }
+ });
+ } else {
+ mColor.setColor(BGCOLOR);
+ }
+
+ mContext.registerReceiver(mReceiver,
+ new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ mContext.unregisterReceiver(mReceiver);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent motion) {
+ return true;
+ }
+
+ @Override
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ // we will be hiding the nav bar, so layout as if it's already hidden
+ return new WindowInsets.Builder(insets).setInsets(
+ Type.systemBars(), Insets.NONE).build();
+ }
+ }
+
+ /**
+ * To get window manager for the display.
+ *
+ * @return the WindowManager specifying with the {@code rootDisplayAreaId} to attach the
+ * confirmation window.
+ */
+ @NonNull
+ private WindowManager createWindowManager(int rootDisplayAreaId) {
+ if (mWindowManager != null) {
+ throw new IllegalStateException(
+ "Must not create a new WindowManager while there is an existing one");
+ }
+ // Create window context to specify the RootDisplayArea
+ final Bundle options = getOptionsForWindowContext(rootDisplayAreaId);
+ mWindowContextRootDisplayAreaId = rootDisplayAreaId;
+ mWindowContext = mDisplayContext.createWindowContext(
+ IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE, options);
+ mWindowManager = mWindowContext.getSystemService(WindowManager.class);
+ return mWindowManager;
+ }
+
+ /**
+ * Returns options that specify the {@link RootDisplayArea} to attach the confirmation window.
+ * {@code null} if the {@code rootDisplayAreaId} is {@link FEATURE_UNDEFINED}.
+ */
+ @Nullable
+ private Bundle getOptionsForWindowContext(int rootDisplayAreaId) {
+ // In case we don't care which root display area the window manager is specifying.
+ if (rootDisplayAreaId == FEATURE_UNDEFINED) {
+ return null;
+ }
+
+ final Bundle options = new Bundle();
+ options.putInt(KEY_ROOT_DISPLAY_AREA_ID, rootDisplayAreaId);
+ return options;
+ }
+
+ private void handleShow(int rootDisplayAreaId) {
+ if (mClingWindow != null) {
+ if (rootDisplayAreaId == mWindowContextRootDisplayAreaId) {
+ if (DEBUG) Log.d(TAG, "Immersive mode confirmation has already been shown");
+ return;
+ } else {
+ // Hide the existing confirmation before show a new one in the new root.
+ if (DEBUG) Log.d(TAG, "Immersive mode confirmation was shown in a different root");
+ handleHide();
+ }
+ }
+ if (DEBUG) Log.d(TAG, "Showing immersive mode confirmation");
+ mClingWindow = new ClingWindowView(mDisplayContext, mConfirm);
+ // show the confirmation
+ final WindowManager.LayoutParams lp = getClingWindowLayoutParams();
+ try {
+ createWindowManager(rootDisplayAreaId).addView(mClingWindow, lp);
+ } catch (WindowManager.InvalidDisplayException e) {
+ Log.w(TAG, "Fail to show the immersive confirmation window because of " + e);
+ }
+ }
+
+ private final Runnable mConfirm = new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) Log.d(TAG, "mConfirm.run()");
+ if (!sConfirmed) {
+ sConfirmed = true;
+ saveSetting(mDisplayContext);
+ }
+ handleHide();
+ }
+ };
+
+ private final class H extends Handler {
+ private static final int SHOW = 1;
+ private static final int HIDE = 2;
+
+ H(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) {
+ return;
+ }
+ switch(msg.what) {
+ case SHOW:
+ handleShow(msg.arg1);
+ break;
+ case HIDE:
+ handleHide();
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onLockTaskModeChanged(int lockTaskState) {
+ mLockTaskState = lockTaskState;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 73d844541259..feb02586a820 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -1035,7 +1035,13 @@ public class KeyguardIndicationController {
return; // udfps affordance is highlighted, no need to show action to unlock
} else if (mKeyguardUpdateMonitor.isFaceEnrolled()
&& !mKeyguardUpdateMonitor.getIsFaceAuthenticated()) {
- String message = mContext.getString(R.string.keyguard_retry);
+ String message;
+ if (mAccessibilityManager.isEnabled()
+ || mAccessibilityManager.isTouchExplorationEnabled()) {
+ message = mContext.getString(R.string.accesssibility_keyguard_retry);
+ } else {
+ message = mContext.getString(R.string.keyguard_retry);
+ }
mStatusBarKeyguardViewManager.setKeyguardMessage(message, mInitialTextColorState);
}
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
index 56390002490c..6e8b8bdebbe3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
@@ -265,6 +265,11 @@ class SystemEventChipAnimationController @Inject constructor(
// not animating then [prepareChipAnimation] will take care of it for us
currentAnimatedView?.let {
updateChipBounds(it, newContentArea)
+ // Since updateCurrentAnimatedView can only be called during an animation, we
+ // have to create a dummy animator here to apply the new chip bounds
+ val animator = ValueAnimator.ofInt(0, 1).setDuration(0)
+ animator.addUpdateListener { updateCurrentAnimatedView() }
+ animator.start()
}
}
})
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
index 62a0d138fd05..5c2f9a8d28ec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
@@ -39,7 +39,10 @@ class StackCoordinator @Inject internal constructor(
override fun attach(pipeline: NotifPipeline) {
pipeline.addOnAfterRenderListListener(::onAfterRenderList)
- groupExpansionManagerImpl.attach(pipeline)
+ // TODO(b/282865576): This has an issue where it makes changes to some groups without
+ // notifying listeners. To be fixed in QPR, but for now let's comment it out to avoid the
+ // group expansion bug.
+ // groupExpansionManagerImpl.attach(pipeline)
}
fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
index 4568c0ca1458..46af03a438f5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
@@ -21,6 +21,8 @@ import androidx.annotation.NonNull;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.statusbar.notification.collection.GroupEntry;
import com.android.systemui.statusbar.notification.collection.ListEntry;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
@@ -44,14 +46,21 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl
private final GroupMembershipManager mGroupMembershipManager;
private final Set<OnGroupExpansionChangeListener> mOnGroupChangeListeners = new HashSet<>();
- // Set of summary keys whose groups are expanded
+ /**
+ * Set of summary keys whose groups are expanded.
+ * NOTE: This should not be modified without notifying listeners, so prefer using
+ * {@code setGroupExpanded} when making changes.
+ */
private final Set<NotificationEntry> mExpandedGroups = new HashSet<>();
+ private final FeatureFlags mFeatureFlags;
+
@Inject
public GroupExpansionManagerImpl(DumpManager dumpManager,
- GroupMembershipManager groupMembershipManager) {
+ GroupMembershipManager groupMembershipManager, FeatureFlags featureFlags) {
mDumpManager = dumpManager;
mGroupMembershipManager = groupMembershipManager;
+ mFeatureFlags = featureFlags;
}
/**
@@ -85,13 +94,17 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl
@Override
public void setGroupExpanded(NotificationEntry entry, boolean expanded) {
final NotificationEntry groupSummary = mGroupMembershipManager.getGroupSummary(entry);
+ boolean changed;
if (expanded) {
- mExpandedGroups.add(groupSummary);
+ changed = mExpandedGroups.add(groupSummary);
} else {
- mExpandedGroups.remove(groupSummary);
+ changed = mExpandedGroups.remove(groupSummary);
}
- sendOnGroupExpandedChange(entry, expanded);
+ // Only notify listeners if something changed.
+ if (!mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE) || changed) {
+ sendOnGroupExpandedChange(entry, expanded);
+ }
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
index a4e8c2ece894..80f5d1939ac0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
@@ -21,12 +21,16 @@ import static com.android.systemui.statusbar.NotificationRemoteInputManager.ENAB
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.statusbar.notification.NotificationUtils.logKey;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.provider.Settings;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.statusbar.IStatusBarService;
@@ -71,6 +75,10 @@ import javax.inject.Named;
@NotificationRowScope
public class ExpandableNotificationRowController implements NotifViewController {
private static final String TAG = "NotifRowController";
+
+ static final Uri BUBBLES_SETTING_URI =
+ Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_BUBBLES);
+ private static final String BUBBLES_SETTING_ENABLED_VALUE = "1";
private final ExpandableNotificationRow mView;
private final NotificationListContainer mListContainer;
private final RemoteInputViewSubcomponent.Factory mRemoteInputViewSubcomponentFactory;
@@ -104,6 +112,23 @@ public class ExpandableNotificationRowController implements NotifViewController
private final ExpandableNotificationRowDragController mDragController;
private final NotificationDismissibilityProvider mDismissibilityProvider;
private final IStatusBarService mStatusBarService;
+
+ private final NotificationSettingsController mSettingsController;
+
+ @VisibleForTesting
+ final NotificationSettingsController.Listener mSettingsListener =
+ new NotificationSettingsController.Listener() {
+ @Override
+ public void onSettingChanged(Uri setting, int userId, String value) {
+ if (BUBBLES_SETTING_URI.equals(setting)) {
+ final int viewUserId = mView.getEntry().getSbn().getUserId();
+ if (viewUserId == UserHandle.USER_ALL || viewUserId == userId) {
+ mView.getPrivateLayout().setBubblesEnabledForUser(
+ BUBBLES_SETTING_ENABLED_VALUE.equals(value));
+ }
+ }
+ }
+ };
private final ExpandableNotificationRow.ExpandableNotificationRowLogger mLoggerCallback =
new ExpandableNotificationRow.ExpandableNotificationRowLogger() {
@Override
@@ -201,6 +226,7 @@ public class ExpandableNotificationRowController implements NotifViewController
FeatureFlags featureFlags,
PeopleNotificationIdentifier peopleNotificationIdentifier,
Optional<BubblesManager> bubblesManagerOptional,
+ NotificationSettingsController settingsController,
ExpandableNotificationRowDragController dragController,
NotificationDismissibilityProvider dismissibilityProvider,
IStatusBarService statusBarService) {
@@ -229,6 +255,7 @@ public class ExpandableNotificationRowController implements NotifViewController
mFeatureFlags = featureFlags;
mPeopleNotificationIdentifier = peopleNotificationIdentifier;
mBubblesManagerOptional = bubblesManagerOptional;
+ mSettingsController = settingsController;
mDragController = dragController;
mMetricsLogger = metricsLogger;
mChildrenContainerLogger = childrenContainerLogger;
@@ -298,12 +325,14 @@ public class ExpandableNotificationRowController implements NotifViewController
NotificationMenuRowPlugin.class, false /* Allow multiple */);
mView.setOnKeyguard(mStatusBarStateController.getState() == KEYGUARD);
mStatusBarStateController.addCallback(mStatusBarStateListener);
+ mSettingsController.addCallback(BUBBLES_SETTING_URI, mSettingsListener);
}
@Override
public void onViewDetachedFromWindow(View v) {
mPluginManager.removePluginListener(mView);
mStatusBarStateController.removeCallback(mStatusBarStateListener);
+ mSettingsController.removeCallback(BUBBLES_SETTING_URI, mSettingsListener);
}
});
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 20f4429f294b..7b6802f95cda 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -44,6 +44,7 @@ import android.widget.LinearLayout;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.R;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.statusbar.RemoteInputController;
import com.android.systemui.statusbar.SmartReplyController;
@@ -65,7 +66,6 @@ import com.android.systemui.statusbar.policy.SmartReplyStateInflaterKt;
import com.android.systemui.statusbar.policy.SmartReplyView;
import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent;
import com.android.systemui.util.Compile;
-import com.android.systemui.wmshell.BubblesManager;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -134,6 +134,7 @@ public class NotificationContentView extends FrameLayout implements Notification
private PeopleNotificationIdentifier mPeopleIdentifier;
private RemoteInputViewSubcomponent.Factory mRemoteInputSubcomponentFactory;
private IStatusBarService mStatusBarService;
+ private boolean mBubblesEnabledForUser;
/**
* List of listeners for when content views become inactive (i.e. not the showing view).
@@ -1440,12 +1441,17 @@ public class NotificationContentView extends FrameLayout implements Notification
}
}
+ @Background
+ public void setBubblesEnabledForUser(boolean enabled) {
+ mBubblesEnabledForUser = enabled;
+ }
+
@VisibleForTesting
boolean shouldShowBubbleButton(NotificationEntry entry) {
boolean isPersonWithShortcut =
mPeopleIdentifier.getPeopleNotificationType(entry)
>= PeopleNotificationIdentifier.TYPE_FULL_PERSON;
- return BubblesManager.areBubblesEnabled(mContext, entry.getSbn().getUser())
+ return mBubblesEnabledForUser
&& isPersonWithShortcut
&& entry.getBubbleMetadata() != null;
}
@@ -2079,6 +2085,7 @@ public class NotificationContentView extends FrameLayout implements Notification
pw.print("null");
}
pw.println();
+ pw.println("mBubblesEnabledForUser: " + mBubblesEnabledForUser);
pw.print("RemoteInputViews { ");
pw.print(" visibleType: " + mVisibleType);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSettingsController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSettingsController.java
new file mode 100644
index 000000000000..585ff523b9a0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSettingsController.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2023 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.row;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.systemui.Dumpable;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.settings.UserTracker;
+import com.android.systemui.util.settings.SecureSettings;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import javax.inject.Inject;
+
+/**
+ * Centralized controller for listening to Secure Settings changes and informing in-process
+ * listeners, on a background thread.
+ */
+@SysUISingleton
+public class NotificationSettingsController implements Dumpable {
+
+ private final static String TAG = "NotificationSettingsController";
+ private final UserTracker mUserTracker;
+ private final UserTracker.Callback mCurrentUserTrackerCallback;
+ private final Handler mHandler;
+ private final ContentObserver mContentObserver;
+ private final SecureSettings mSecureSettings;
+ private final HashMap<Uri, ArrayList<Listener>> mListeners = new HashMap<>();
+
+ @Inject
+ public NotificationSettingsController(UserTracker userTracker,
+ @Background Handler handler,
+ SecureSettings secureSettings,
+ DumpManager dumpManager) {
+ mUserTracker = userTracker;
+ mHandler = handler;
+ mSecureSettings = secureSettings;
+ mContentObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ super.onChange(selfChange, uri);
+ synchronized (mListeners) {
+ if (mListeners.containsKey(uri)) {
+ for (Listener listener : mListeners.get(uri)) {
+ notifyListener(listener, uri);
+ }
+ }
+ }
+ }
+ };
+
+ mCurrentUserTrackerCallback = new UserTracker.Callback() {
+ @Override
+ public void onUserChanged(int newUser, Context userContext) {
+ synchronized (mListeners) {
+ if (mListeners.size() > 0) {
+ mSecureSettings.unregisterContentObserver(mContentObserver);
+ for (Uri uri : mListeners.keySet()) {
+ mSecureSettings.registerContentObserverForUser(
+ uri, false, mContentObserver, newUser);
+ }
+ }
+ }
+ }
+ };
+ mUserTracker.addCallback(mCurrentUserTrackerCallback, new HandlerExecutor(handler));
+
+ dumpManager.registerNormalDumpable(TAG, this);
+ }
+
+ /**
+ * Register callback whenever the given secure settings changes.
+ *
+ * On registration, will call back on the provided handler with the current value of
+ * the setting.
+ */
+ public void addCallback(@NonNull Uri uri, @NonNull Listener listener) {
+ if (uri == null || listener == null) {
+ return;
+ }
+ synchronized (mListeners) {
+ ArrayList<Listener> currentListeners = mListeners.get(uri);
+ if (currentListeners == null) {
+ currentListeners = new ArrayList<>();
+ }
+ if (!currentListeners.contains(listener)) {
+ currentListeners.add(listener);
+ }
+ mListeners.put(uri, currentListeners);
+ if (currentListeners.size() == 1) {
+ mSecureSettings.registerContentObserverForUser(
+ uri, false, mContentObserver, mUserTracker.getUserId());
+ }
+ }
+ mHandler.post(() -> notifyListener(listener, uri));
+
+ }
+
+ public void removeCallback(Uri uri, Listener listener) {
+ synchronized (mListeners) {
+ ArrayList<Listener> currentListeners = mListeners.get(uri);
+
+ if (currentListeners != null) {
+ currentListeners.remove(listener);
+ }
+ if (currentListeners == null || currentListeners.size() == 0) {
+ mListeners.remove(uri);
+ }
+
+ if (mListeners.size() == 0) {
+ mSecureSettings.unregisterContentObserver(mContentObserver);
+ }
+ }
+ }
+
+ @Override
+ public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+ synchronized (mListeners) {
+ pw.println("Settings Uri Listener List:");
+ for (Uri uri : mListeners.keySet()) {
+ pw.println(" Uri=" + uri);
+ for (Listener listener : mListeners.get(uri)) {
+ pw.println(" Listener=" + listener.getClass().getName());
+ }
+ }
+ }
+ }
+
+ private void notifyListener(Listener listener, Uri uri) {
+ final String setting = uri == null ? null : uri.getLastPathSegment();
+ int userId = mUserTracker.getUserId();
+ listener.onSettingChanged(uri, userId, mSecureSettings.getStringForUser(setting, userId));
+ }
+
+ /**
+ * Listener invoked whenever settings are changed.
+ */
+ public interface Listener {
+ void onSettingChanged(@NonNull Uri setting, int userId, @Nullable String value);
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index ccb51898a333..58d6bb024476 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.phone;
import static android.app.StatusBarManager.SESSION_KEYGUARD;
+import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
import static com.android.systemui.keyguard.WakefulnessLifecycle.UNKNOWN_LAST_WAKE_TIME;
import android.annotation.IntDef;
@@ -30,6 +31,7 @@ import android.metrics.LogMaker;
import android.os.Handler;
import android.os.PowerManager;
import android.os.Trace;
+import android.view.HapticFeedbackConstants;
import androidx.annotation.Nullable;
@@ -51,6 +53,7 @@ import com.android.systemui.biometrics.AuthController;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
@@ -177,6 +180,8 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
private long mLastFpFailureUptimeMillis;
private int mNumConsecutiveFpFailures;
+ private final FeatureFlags mFeatureFlags;
+
private static final class PendingAuthenticated {
public final int userId;
public final BiometricSourceType biometricSourceType;
@@ -280,7 +285,8 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
LatencyTracker latencyTracker,
ScreenOffAnimationController screenOffAnimationController,
VibratorHelper vibrator,
- SystemClock systemClock
+ SystemClock systemClock,
+ FeatureFlags featureFlags
) {
mPowerManager = powerManager;
mUpdateMonitor = keyguardUpdateMonitor;
@@ -308,6 +314,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
mVibratorHelper = vibrator;
mLogger = biometricUnlockLogger;
mSystemClock = systemClock;
+ mFeatureFlags = featureFlags;
dumpManager.registerDumpable(getClass().getName(), this);
}
@@ -750,8 +757,15 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
mLogger.d("Skip auth success haptic. Power button was recently pressed.");
return;
}
- mVibratorHelper.vibrateAuthSuccess(
- getClass().getSimpleName() + ", type =" + type + "device-entry::success");
+ if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+ mVibratorHelper.performHapticFeedback(
+ mKeyguardViewController.getViewRootImpl().getView(),
+ HapticFeedbackConstants.CONFIRM
+ );
+ } else {
+ mVibratorHelper.vibrateAuthSuccess(
+ getClass().getSimpleName() + ", type =" + type + "device-entry::success");
+ }
}
private boolean lastWakeupFromPowerButtonWithinHapticThreshold() {
@@ -764,8 +778,15 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
}
private void vibrateError(BiometricSourceType type) {
- mVibratorHelper.vibrateAuthError(
- getClass().getSimpleName() + ", type =" + type + "device-entry::error");
+ if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+ mVibratorHelper.performHapticFeedback(
+ mKeyguardViewController.getViewRootImpl().getView(),
+ HapticFeedbackConstants.REJECT
+ );
+ } else {
+ mVibratorHelper.vibrateAuthError(
+ getClass().getSimpleName() + ", type =" + type + "device-entry::error");
+ }
}
private void cleanup() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
index 862f169b2176..4e136deab5e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -28,6 +28,9 @@ import com.android.systemui.Gefingerpoken
import com.android.systemui.R
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.RemoteUserInput
+import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.shade.ShadeController
import com.android.systemui.shade.ShadeLogger
import com.android.systemui.shade.ShadeViewController
@@ -43,6 +46,7 @@ import com.android.systemui.util.view.ViewUtil
import java.util.Optional
import javax.inject.Inject
import javax.inject.Named
+import javax.inject.Provider
private const val TAG = "PhoneStatusBarViewController"
@@ -53,10 +57,12 @@ class PhoneStatusBarViewController private constructor(
private val centralSurfaces: CentralSurfaces,
private val shadeController: ShadeController,
private val shadeViewController: ShadeViewController,
+ private val sceneInteractor: Provider<SceneInteractor>,
private val shadeLogger: ShadeLogger,
private val moveFromCenterAnimationController: StatusBarMoveFromCenterAnimationController?,
private val userChipViewModel: StatusBarUserChipViewModel,
private val viewUtil: ViewUtil,
+ private val featureFlags: FeatureFlags,
private val configurationController: ConfigurationController
) : ViewController<PhoneStatusBarView>(view) {
@@ -164,6 +170,16 @@ class PhoneStatusBarViewController private constructor(
return false
}
+ // If scene framework is enabled, route the touch to it and
+ // ignore the rest of the gesture.
+ if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
+ sceneInteractor.get()
+ .onRemoteUserInput(RemoteUserInput.translateMotionEvent(event))
+ // TODO(b/291965119): remove once view is expanded to cover the status bar
+ sceneInteractor.get().setVisible(SceneContainerNames.SYSTEM_UI_DEFAULT, true)
+ return false
+ }
+
if (event.action == MotionEvent.ACTION_DOWN) {
// If the view that would receive the touch is disabled, just have status
// bar eat the gesture.
@@ -225,6 +241,7 @@ class PhoneStatusBarViewController private constructor(
private val centralSurfaces: CentralSurfaces,
private val shadeController: ShadeController,
private val shadeViewController: ShadeViewController,
+ private val sceneInteractor: Provider<SceneInteractor>,
private val shadeLogger: ShadeLogger,
private val viewUtil: ViewUtil,
private val configurationController: ConfigurationController,
@@ -245,10 +262,12 @@ class PhoneStatusBarViewController private constructor(
centralSurfaces,
shadeController,
shadeViewController,
+ sceneInteractor,
shadeLogger,
statusBarMoveFromCenterAnimationController,
userChipViewModel,
viewUtil,
+ featureFlags,
configurationController
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
index 2a039dade059..68a6b3d62bae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
@@ -34,15 +34,21 @@ import com.android.systemui.Dumpable;
import com.android.systemui.R;
import com.android.systemui.ScreenDecorations;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.scene.domain.interactor.SceneInteractor;
+import com.android.systemui.scene.shared.model.SceneContainerNames;
import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
+import com.android.systemui.util.kotlin.JavaAdapter;
import java.io.PrintWriter;
import javax.inject.Inject;
+import javax.inject.Provider;
/**
* Manages what parts of the status bar are touchable. Clients are primarily UI that display in the
@@ -78,6 +84,9 @@ public final class StatusBarTouchableRegionManager implements Dumpable {
ConfigurationController configurationController,
HeadsUpManagerPhone headsUpManager,
ShadeExpansionStateManager shadeExpansionStateManager,
+ Provider<SceneInteractor> sceneInteractor,
+ Provider<JavaAdapter> javaAdapter,
+ FeatureFlags featureFlags,
UnlockedScreenOffAnimationController unlockedScreenOffAnimationController
) {
mContext = context;
@@ -115,6 +124,12 @@ public final class StatusBarTouchableRegionManager implements Dumpable {
mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
shadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
+ if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
+ javaAdapter.get().alwaysCollectFlow(
+ sceneInteractor.get().isVisible(SceneContainerNames.SYSTEM_UI_DEFAULT),
+ this::onShadeExpansionFullyChanged);
+ }
+
mOnComputeInternalInsetsListener = this::onComputeInternalInsets;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
index 7e9172da1817..e8da9519c7ef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
@@ -188,7 +188,7 @@ class UnlockedScreenOffAnimationController @Inject constructor(
// Cancel any existing CUJs before starting the animation
interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
-
+ PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.ALPHA)
PropertyAnimator.setProperty(
keyguardView, AnimatableProperty.ALPHA, 1f,
AnimationProperties()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index 02b7e9176cf2..e00365d8fbcb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -30,6 +30,7 @@ import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.GradientDrawable;
+import android.os.Trace;
import android.os.UserHandle;
import android.text.Editable;
import android.text.SpannedString;
@@ -1032,10 +1033,12 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
}
private void hideIme() {
+ Trace.beginSection("RemoteEditText#hideIme");
final WindowInsetsController insetsController = getWindowInsetsController();
if (insetsController != null) {
insetsController.hide(WindowInsets.Type.ime());
}
+ Trace.endSection();
}
private void defocusIfNeeded(boolean animate) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
index 9cabd35cb1e5..5766f1be8894 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
@@ -18,6 +18,7 @@ package com.android.systemui.biometrics
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
+import androidx.test.filters.RequiresDevice
import com.android.systemui.SysuiTestCase
import com.android.systemui.shade.ShadeExpansionStateManager
import org.junit.Assert
@@ -30,6 +31,7 @@ import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.junit.MockitoJUnit
+@RequiresDevice
@SmallTest
@RunWith(AndroidTestingRunner::class)
class AuthDialogPanelInteractionDetectorTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt
new file mode 100644
index 000000000000..fcc40404bf7d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2023 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.biometrics.data.repository
+
+import android.hardware.biometrics.SensorProperties
+import android.hardware.face.FaceManager
+import android.hardware.face.FaceSensorPropertiesInternal
+import android.hardware.face.IFaceAuthenticatorsRegisteredCallback
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.coroutines.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+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.Mock
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class FacePropertyRepositoryImplTest : SysuiTestCase() {
+ @JvmField @Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ private lateinit var underTest: FacePropertyRepository
+ private lateinit var testScope: TestScope
+
+ @Captor private lateinit var callback: ArgumentCaptor<IFaceAuthenticatorsRegisteredCallback>
+ @Mock private lateinit var faceManager: FaceManager
+ @Before
+ fun setup() {
+ testScope = TestScope()
+ underTest = createRepository(faceManager)
+ }
+
+ private fun createRepository(manager: FaceManager? = faceManager) =
+ FacePropertyRepositoryImpl(testScope.backgroundScope, manager)
+
+ @Test
+ fun whenFaceManagerIsNotPresentIsNull() =
+ testScope.runTest {
+ underTest = createRepository(null)
+ val sensor = collectLastValue(underTest.sensorInfo)
+
+ assertThat(sensor()).isNull()
+ }
+
+ @Test
+ fun providesTheValuePassedToTheAuthenticatorsRegisteredCallback() {
+ testScope.runTest {
+ val sensor by collectLastValue(underTest.sensorInfo)
+ runCurrent()
+ verify(faceManager).addAuthenticatorsRegisteredCallback(callback.capture())
+
+ callback.value.onAllAuthenticatorsRegistered(
+ listOf(createSensorProperties(1, SensorProperties.STRENGTH_STRONG))
+ )
+
+ assertThat(sensor).isEqualTo(FaceSensorInfo(1, SensorStrength.STRONG))
+ }
+ }
+
+ private fun createSensorProperties(id: Int, strength: Int) =
+ FaceSensorPropertiesInternal(id, strength, 0, emptyList(), 1, false, false, false)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 45d1af722369..8edc6cf8dd54 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -29,6 +29,7 @@ import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -77,7 +78,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
SceneTestUtils.CONTAINER_1,
@@ -88,7 +89,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onShown()
assertThat(message?.text).isEqualTo(ENTER_YOUR_PIN)
- assertThat(entries).hasSize(0)
+ assertThat(pin).isEmpty()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@@ -98,7 +99,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
@@ -112,8 +113,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onPinButtonClicked(1)
assertThat(message?.text).isEmpty()
- assertThat(entries).hasSize(1)
- assertThat(entries?.map { it.input }).containsExactly(1)
+ assertThat(pin).containsExactly(1)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@@ -123,7 +123,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
@@ -134,12 +134,12 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onShown()
runCurrent()
underTest.onPinButtonClicked(1)
- assertThat(entries).hasSize(1)
+ assertThat(pin).hasSize(1)
underTest.onBackspaceButtonClicked()
assertThat(message?.text).isEmpty()
- assertThat(entries).hasSize(0)
+ assertThat(pin).isEmpty()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@@ -148,7 +148,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
testScope.runTest {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
@@ -166,9 +166,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onPinButtonClicked(4)
underTest.onPinButtonClicked(5)
- assertThat(entries).hasSize(3)
- assertThat(entries?.map { it.input }).containsExactly(1, 4, 5).inOrder()
- assertThat(entries?.map { it.sequenceNumber }).isInStrictOrder()
+ assertThat(pin).containsExactly(1, 4, 5).inOrder()
}
@Test
@@ -177,7 +175,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
@@ -195,7 +193,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onBackspaceButtonLongPressed()
assertThat(message?.text).isEmpty()
- assertThat(entries).hasSize(0)
+ assertThat(pin).isEmpty()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@@ -227,7 +225,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
@@ -244,7 +242,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onAuthenticateButtonClicked()
- assertThat(entries).hasSize(0)
+ assertThat(pin).isEmpty()
assertThat(message?.text).isEqualTo(WRONG_PIN)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@@ -255,7 +253,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
@@ -271,7 +269,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onPinButtonClicked(5) // PIN is now wrong!
underTest.onAuthenticateButtonClicked()
assertThat(message?.text).isEqualTo(WRONG_PIN)
- assertThat(entries).hasSize(0)
+ assertThat(pin).isEmpty()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
// Enter the correct PIN:
@@ -312,7 +310,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
utils.authenticationRepository.setAutoConfirmEnabled(true)
@@ -329,7 +327,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
FakeAuthenticationRepository.DEFAULT_PIN.last() + 1
) // PIN is now wrong!
- assertThat(entries).hasSize(0)
+ assertThat(pin).isEmpty()
assertThat(message?.text).isEqualTo(WRONG_PIN)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
new file mode 100644
index 000000000000..4c279ea08fd7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
@@ -0,0 +1,277 @@
+package com.android.systemui.bouncer.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.ui.viewmodel.EntryToken.ClearAll
+import com.android.systemui.bouncer.ui.viewmodel.EntryToken.Digit
+import com.android.systemui.bouncer.ui.viewmodel.PinInputSubject.Companion.assertThat
+import com.android.systemui.bouncer.ui.viewmodel.PinInputViewModel.Companion.empty
+import com.google.common.truth.Fact
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.Subject
+import com.google.common.truth.Subject.Factory
+import com.google.common.truth.Truth.assertAbout
+import com.google.common.truth.Truth.assertThat
+import java.lang.Character.isDigit
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/**
+ * This test uses a mnemonic code to create and verify PinInput instances: strings of digits [0-9]
+ * for [Digit] tokens, as well as a `C` for the [ClearAll] token.
+ */
+@SmallTest
+@RunWith(JUnit4::class)
+class PinInputViewModelTest : SysuiTestCase() {
+
+ @Test
+ fun create_emptyList_throws() {
+ assertThrows(IllegalArgumentException::class.java) { PinInputViewModel(emptyList()) }
+ }
+
+ @Test
+ fun create_inputWithoutLeadingClearAll_throws() {
+ val exception =
+ assertThrows(IllegalArgumentException::class.java) {
+ PinInputViewModel(listOf(Digit(0)))
+ }
+ assertThat(exception).hasMessageThat().contains("does not begin with a ClearAll token")
+ }
+
+ @Test
+ fun create_inputNotInAscendingOrder_throws() {
+ val sentinel = ClearAll()
+ val first = Digit(0)
+ val second = Digit(1)
+ // [first] is created before [second] is created, thus their sequence numbers are ordered.
+ check(first.sequenceNumber < second.sequenceNumber)
+
+ val exception =
+ assertThrows(IllegalArgumentException::class.java) {
+ // Passing the [Digit] tokens in reverse order throws.
+ PinInputViewModel(listOf(sentinel, second, first))
+ }
+ assertThat(exception).hasMessageThat().contains("EntryTokens are not sorted")
+ }
+
+ @Test
+ fun append_digitToEmptyInput() {
+ val result = empty().append(0)
+ assertThat(result).matches("C0")
+ }
+
+ @Test
+ fun append_digitToExistingPin() {
+ val subject = pinInput("C1")
+ assertThat(subject.append(2)).matches("C12")
+ }
+
+ @Test
+ fun append_withTwoCompletePinsEntered_dropsFirst() {
+ val subject = pinInput("C12C34C")
+ assertThat(subject.append(5)).matches("C34C5")
+ }
+
+ @Test
+ fun deleteLast_removesLastDigit() {
+ val subject = pinInput("C12")
+ assertThat(subject.deleteLast()).matches("C1")
+ }
+
+ @Test
+ fun deleteLast_onEmptyInput_returnsSameInstance() {
+ val subject = empty()
+ assertThat(subject.deleteLast()).isSameInstanceAs(subject)
+ }
+
+ @Test
+ fun deleteLast_onInputEndingInClearAll_returnsSameInstance() {
+ val subject = pinInput("C12C")
+ assertThat(subject.deleteLast()).isSameInstanceAs(subject)
+ }
+
+ @Test
+ fun clearAll_appendsClearAllEntryToExistingInput() {
+ val subject = pinInput("C12")
+ assertThat(subject.clearAll()).matches("C12C")
+ }
+
+ @Test
+ fun clearAll_onInputEndingInClearAll_returnsSameInstance() {
+ val subject = pinInput("C12C")
+ assertThat(subject.clearAll()).isSameInstanceAs(subject)
+ }
+
+ @Test
+ fun clearAll_retainsUpToTwoPinEntries() {
+ val subject = pinInput("C12C34")
+ assertThat(subject.clearAll()).matches("C12C34C")
+ }
+
+ @Test
+ fun isEmpty_onEmptyInput_returnsTrue() {
+ val subject = empty()
+ assertThat(subject.isEmpty()).isTrue()
+ }
+
+ @Test
+ fun isEmpty_whenLastEntryIsDigit_returnsFalse() {
+ val subject = pinInput("C1234")
+ assertThat(subject.isEmpty()).isFalse()
+ }
+
+ @Test
+ fun isEmpty_whenLastEntryIsClearAll_returnsTrue() {
+ val subject = pinInput("C1234C")
+ assertThat(subject.isEmpty()).isTrue()
+ }
+
+ @Test
+ fun getPin_onEmptyInput_returnsEmptyList() {
+ val subject = empty()
+ assertThat(subject.getPin()).isEmpty()
+ }
+
+ @Test
+ fun getPin_whenLastEntryIsDigit_returnsPin() {
+ val subject = pinInput("C1234")
+ assertThat(subject.getPin()).containsExactly(1, 2, 3, 4)
+ }
+
+ @Test
+ fun getPin_withMultiplePins_returnsLastPin() {
+ val subject = pinInput("C1234C5678")
+ assertThat(subject.getPin()).containsExactly(5, 6, 7, 8)
+ }
+
+ @Test
+ fun getPin_whenLastEntryIsClearAll_returnsEmptyList() {
+ val subject = pinInput("C1234C")
+ assertThat(subject.getPin()).isEmpty()
+ }
+
+ @Test
+ fun mostRecentClearAllMarker_onEmptyInput_returnsSentinel() {
+ val subject = empty()
+ val sentinel = subject.input[0] as ClearAll
+
+ assertThat(subject.mostRecentClearAll()).isSameInstanceAs(sentinel)
+ }
+
+ @Test
+ fun mostRecentClearAllMarker_whenLastEntryIsDigit_returnsSentinel() {
+ val subject = pinInput("C1234")
+ val sentinel = subject.input[0] as ClearAll
+
+ assertThat(subject.mostRecentClearAll()).isSameInstanceAs(sentinel)
+ }
+
+ @Test
+ fun mostRecentClearAllMarker_withMultiplePins_returnsLastMarker() {
+ val subject = pinInput("C1234C5678")
+ val lastMarker = subject.input[5] as ClearAll
+
+ assertThat(subject.mostRecentClearAll()).isSameInstanceAs(lastMarker)
+ }
+
+ @Test
+ fun mostRecentClearAllMarker_whenLastEntryIsClearAll_returnsLastEntry() {
+ val subject = pinInput("C1234C")
+ val lastEntry = subject.input[5] as ClearAll
+
+ assertThat(subject.mostRecentClearAll()).isSameInstanceAs(lastEntry)
+ }
+
+ @Test
+ fun getDigits_invalidClearAllMarker_onEmptyInput_returnsEmptyList() {
+ val subject = empty()
+ assertThat(subject.getDigits(ClearAll())).isEmpty()
+ }
+
+ @Test
+ fun getDigits_invalidClearAllMarker_whenLastEntryIsDigit_returnsEmptyList() {
+ val subject = pinInput("C1234")
+ assertThat(subject.getDigits(ClearAll())).isEmpty()
+ }
+
+ @Test
+ fun getDigits_clearAllMarkerPointsToFirstPin_returnsFirstPinDigits() {
+ val subject = pinInput("C1234C5678")
+ val marker = subject.input[0] as ClearAll
+
+ assertThat(subject.getDigits(marker).map { it.input }).containsExactly(1, 2, 3, 4)
+ }
+
+ @Test
+ fun getDigits_clearAllMarkerPointsToLastPin_returnsLastPinDigits() {
+ val subject = pinInput("C1234C5678")
+ val marker = subject.input[5] as ClearAll
+
+ assertThat(subject.getDigits(marker).map { it.input }).containsExactly(5, 6, 7, 8)
+ }
+
+ @Test
+ fun entryToken_equality() {
+ val clearAll = ClearAll()
+ val zero = Digit(0)
+ val one = Digit(1)
+
+ // Guava's EqualsTester is not available in this codebase.
+ assertThat(zero.equals(zero.copy())).isTrue()
+
+ assertThat(zero.equals(one)).isFalse()
+ assertThat(zero.equals(clearAll)).isFalse()
+
+ assertThat(clearAll.equals(clearAll.copy())).isTrue()
+ assertThat(clearAll.equals(zero)).isFalse()
+
+ // Not equal when the sequence number does not match
+ assertThat(zero.equals(Digit(0))).isFalse()
+ assertThat(clearAll.equals(ClearAll())).isFalse()
+ }
+
+ private fun pinInput(mnemonics: String): PinInputViewModel {
+ return PinInputViewModel(
+ mnemonics.map {
+ when {
+ it == 'C' -> ClearAll()
+ isDigit(it) -> Digit(it.digitToInt())
+ else -> throw AssertionError()
+ }
+ }
+ )
+ }
+}
+
+private class PinInputSubject
+private constructor(metadata: FailureMetadata, private val actual: PinInputViewModel) :
+ Subject(metadata, actual) {
+
+ fun matches(mnemonics: String) {
+ val actualMnemonics =
+ actual.input
+ .map { entry ->
+ when (entry) {
+ is Digit -> entry.input.digitToChar()
+ is ClearAll -> 'C'
+ else -> throw IllegalArgumentException()
+ }
+ }
+ .joinToString(separator = "")
+
+ if (mnemonics != actualMnemonics) {
+ failWithActual(
+ Fact.simpleFact(
+ "expected pin input to be '$mnemonics' but is '$actualMnemonics' instead"
+ )
+ )
+ }
+ }
+
+ companion object {
+ fun assertThat(input: PinInputViewModel): PinInputSubject =
+ assertAbout(Factory(::PinInputSubject)).that(input)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java
index 2a4c0eb18d02..7628be44755d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java
@@ -126,7 +126,8 @@ public class IntentCreatorTest extends SysuiTestCase {
assertEquals(Intent.ACTION_CHOOSER, intent.getAction());
assertFlags(intent, EXTERNAL_INTENT_FLAGS);
Intent target = intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent.class);
- assertEquals(uri, target.getData());
+ assertEquals(uri, target.getParcelableExtra(Intent.EXTRA_STREAM, Uri.class));
+ assertEquals(uri, target.getClipData().getItemAt(0).getUri());
assertEquals("image/png", target.getType());
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
index 925ac30b99fd..05d6b99fe227 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
@@ -16,6 +16,7 @@ import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.shared.model.WakeSleepReason
import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.keyguard.shared.model.WakefulnessState
+import com.android.systemui.util.mockito.any
import com.android.systemui.utils.GlobalWindowManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
@@ -225,6 +226,9 @@ class ResourceTrimmerTest : SysuiTestCase() {
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
)
- verifyNoMoreInteractions(globalWindowManager)
+ // Memory hidden should still be called.
+ verify(globalWindowManager, times(1))
+ .trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
+ verify(globalWindowManager, times(0)).trimCaches(any())
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
index b3f800087bdf..01a6c64a6898 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
@@ -40,6 +40,9 @@ import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.R
import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FaceSensorInfo
+import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
+import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.coroutines.FlowValue
@@ -151,6 +154,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
private lateinit var fakeCommandQueue: FakeCommandQueue
private lateinit var featureFlags: FakeFeatureFlags
+ private lateinit var fakeFacePropertyRepository: FakeFacePropertyRepository
private var wasAuthCancelled = false
private var wasDetectCancelled = false
@@ -224,6 +228,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
repository = keyguardTransitionRepository,
)
.keyguardTransitionInteractor
+ fakeFacePropertyRepository = FakeFacePropertyRepository()
return DeviceEntryFaceAuthRepositoryImpl(
mContext,
fmOverride,
@@ -245,6 +250,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
faceAuthBuffer,
keyguardTransitionInteractor,
featureFlags,
+ fakeFacePropertyRepository,
dumpManager,
)
}
@@ -591,6 +597,17 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
}
@Test
+ fun authenticateDoesNotRunWhenStrongBiometricIsNotAllowedAndFaceSensorIsStrong() =
+ testScope.runTest {
+ fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.STRONG))
+ runCurrent()
+
+ testGatingCheckForFaceAuth(isFaceStrong = true) {
+ biometricSettingsRepository.setIsStrongBiometricAllowed(false)
+ }
+ }
+
+ @Test
fun authenticateDoesNotRunWhenSecureCameraIsActive() =
testScope.runTest {
testGatingCheckForFaceAuth {
@@ -923,6 +940,19 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
}
@Test
+ fun detectDoesNotRunWhenStrongBiometricIsAllowedAndFaceAuthSensorStrengthIsStrong() =
+ testScope.runTest {
+ fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.STRONG))
+ runCurrent()
+
+ testGatingCheckForDetect(isFaceStrong = true) {
+ biometricSettingsRepository.setIsStrongBiometricAllowed(true)
+ // this shouldn't matter as face is set as a strong sensor
+ biometricSettingsRepository.setIsNonStrongBiometricAllowed(false)
+ }
+ }
+
+ @Test
fun detectDoesNotRunIfUdfpsIsRunning() =
testScope.runTest {
testGatingCheckForDetect {
@@ -1013,9 +1043,12 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
faceAuthenticateIsCalled()
}
- private suspend fun TestScope.testGatingCheckForFaceAuth(gatingCheckModifier: () -> Unit) {
+ private suspend fun TestScope.testGatingCheckForFaceAuth(
+ isFaceStrong: Boolean = false,
+ gatingCheckModifier: () -> Unit
+ ) {
initCollectors()
- allPreconditionsToRunFaceAuthAreTrue()
+ allPreconditionsToRunFaceAuthAreTrue(isFaceStrong)
gatingCheckModifier()
runCurrent()
@@ -1024,7 +1057,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
assertThat(underTest.canRunFaceAuth.value).isFalse()
// flip the gating check back on.
- allPreconditionsToRunFaceAuthAreTrue()
+ allPreconditionsToRunFaceAuthAreTrue(isFaceStrong)
triggerFaceAuth(false)
@@ -1043,12 +1076,19 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
faceAuthenticateIsNotCalled()
}
- private suspend fun TestScope.testGatingCheckForDetect(gatingCheckModifier: () -> Unit) {
+ private suspend fun TestScope.testGatingCheckForDetect(
+ isFaceStrong: Boolean = false,
+ gatingCheckModifier: () -> Unit
+ ) {
initCollectors()
allPreconditionsToRunFaceAuthAreTrue()
- // This will stop face auth from running but is required to be false for detect.
- biometricSettingsRepository.setIsNonStrongBiometricAllowed(false)
+ if (isFaceStrong) {
+ biometricSettingsRepository.setStrongBiometricAllowed(false)
+ } else {
+ // This will stop face auth from running but is required to be false for detect.
+ biometricSettingsRepository.setIsNonStrongBiometricAllowed(false)
+ }
runCurrent()
assertThat(canFaceAuthRun()).isFalse()
@@ -1083,7 +1123,9 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
cancellationSignal.value.setOnCancelListener { wasAuthCancelled = true }
}
- private suspend fun TestScope.allPreconditionsToRunFaceAuthAreTrue() {
+ private suspend fun TestScope.allPreconditionsToRunFaceAuthAreTrue(
+ isFaceStrong: Boolean = false
+ ) {
verify(faceManager, atLeastOnce())
.addLockoutResetCallback(faceLockoutResetCallback.capture())
biometricSettingsRepository.setFaceEnrolled(true)
@@ -1098,7 +1140,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
WakeSleepReason.OTHER
)
)
- biometricSettingsRepository.setIsNonStrongBiometricAllowed(true)
+ if (isFaceStrong) {
+ biometricSettingsRepository.setStrongBiometricAllowed(true)
+ } else {
+ biometricSettingsRepository.setIsNonStrongBiometricAllowed(true)
+ }
biometricSettingsRepository.setIsUserInLockdown(false)
fakeUserRepository.setSelectedUserInfo(primaryUser)
biometricSettingsRepository.setIsFaceAuthSupportedInCurrentPosture(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
index 8c9ed5b2ef4e..8636dd8df3b0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
@@ -342,12 +342,13 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() {
}
@Test
- fun faceUnlockIsDisabledWhenFpIsLockedOut() = testScope.runTest {
- underTest.start()
+ fun faceUnlockIsDisabledWhenFpIsLockedOut() =
+ testScope.runTest {
+ underTest.start()
- fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
- runCurrent()
+ fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
+ runCurrent()
- assertThat(faceAuthRepository.wasDisabled).isTrue()
- }
+ assertThat(faceAuthRepository.wasDisabled).isTrue()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
index 611c5b987d84..fab1de00dcbc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
@@ -20,6 +20,7 @@ import android.os.Handler
import android.os.Looper
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
+import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_MOVE
@@ -29,6 +30,8 @@ import android.view.WindowManager
import androidx.test.filters.SmallTest
import com.android.internal.util.LatencyTracker
import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
import com.android.systemui.plugins.NavigationEdgeBackPlugin
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -36,6 +39,8 @@ import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.verify
@@ -58,6 +63,7 @@ class BackPanelControllerTest : SysuiTestCase() {
@Mock private lateinit var latencyTracker: LatencyTracker
@Mock private lateinit var layoutParams: WindowManager.LayoutParams
@Mock private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback
+ private val featureFlags = FakeFeatureFlags()
@Before
fun setup() {
@@ -70,7 +76,8 @@ class BackPanelControllerTest : SysuiTestCase() {
Handler.createAsync(Looper.myLooper()),
vibratorHelper,
configurationController,
- latencyTracker
+ latencyTracker,
+ featureFlags
)
mBackPanelController.setLayoutParams(layoutParams)
mBackPanelController.setBackCallback(backCallback)
@@ -99,6 +106,7 @@ class BackPanelControllerTest : SysuiTestCase() {
@Test
fun handlesBackCommitted() {
+ featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false)
startTouch()
// Move once to cross the touch slop
continueTouch(START_X + touchSlop.toFloat() + 1)
@@ -122,7 +130,34 @@ class BackPanelControllerTest : SysuiTestCase() {
}
@Test
+ fun handlesBackCommitted_withOneWayHapticsAPI() {
+ featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true)
+ startTouch()
+ // Move once to cross the touch slop
+ continueTouch(START_X + touchSlop.toFloat() + 1)
+ // Move again to cross the back trigger threshold
+ continueTouch(START_X + touchSlop + triggerThreshold + 1)
+ // Wait threshold duration and hold touch past trigger threshold
+ Thread.sleep((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong())
+ continueTouch(START_X + touchSlop + triggerThreshold + 1)
+
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.ACTIVE)
+ verify(backCallback).setTriggerBack(true)
+ testableLooper.moveTimeForward(100)
+ testableLooper.processAllMessages()
+ verify(vibratorHelper)
+ .performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE))
+
+ finishTouchActionUp(START_X + touchSlop + triggerThreshold + 1)
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.COMMITTED)
+ verify(backCallback).triggerBack()
+ }
+
+ @Test
fun handlesBackCancelled() {
+ featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false)
startTouch()
// Move once to cross the touch slop
continueTouch(START_X + touchSlop.toFloat() + 1)
@@ -151,6 +186,38 @@ class BackPanelControllerTest : SysuiTestCase() {
verify(backCallback).cancelBack()
}
+ @Test
+ fun handlesBackCancelled_withOneWayHapticsAPI() {
+ featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true)
+ startTouch()
+ // Move once to cross the touch slop
+ continueTouch(START_X + touchSlop.toFloat() + 1)
+ // Move again to cross the back trigger threshold
+ continueTouch(
+ START_X + touchSlop + triggerThreshold -
+ mBackPanelController.params.deactivationTriggerThreshold
+ )
+ // Wait threshold duration and hold touch before trigger threshold
+ Thread.sleep((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong())
+ continueTouch(
+ START_X + touchSlop + triggerThreshold -
+ mBackPanelController.params.deactivationTriggerThreshold
+ )
+ clearInvocations(backCallback)
+ Thread.sleep(MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION)
+ // Move in the opposite direction to cross the deactivation threshold and cancel back
+ continueTouch(START_X)
+
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.INACTIVE)
+ verify(backCallback).setTriggerBack(false)
+ verify(vibratorHelper)
+ .performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE))
+
+ finishTouchActionUp(START_X)
+ verify(backCallback).cancelBack()
+ }
+
private fun startTouch() {
mBackPanelController.onMotionEvent(createMotionEvent(ACTION_DOWN, START_X, 0f))
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
index a76af8e83248..c65a2d36e223 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -118,6 +118,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
whenever(context.getString(eq(R.string.note_task_shortcut_long_label), any()))
.thenReturn(NOTE_TASK_LONG_LABEL)
whenever(context.packageManager).thenReturn(packageManager)
+ whenever(context.createContextAsUser(any(), any())).thenReturn(context)
whenever(packageManager.getApplicationInfo(any(), any<Int>())).thenReturn(mock())
whenever(packageManager.getApplicationLabel(any())).thenReturn(NOTE_TASK_LONG_LABEL)
whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(NOTE_TASK_INFO)
@@ -353,7 +354,13 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
@Test
fun showNoteTask_defaultUserSet_shouldStartActivityWithExpectedUserAndLogUiEvent() {
- whenever(secureSettings.getInt(eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE), any()))
+ whenever(
+ secureSettings.getIntForUser(
+ /* name= */ eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE),
+ /* def= */ any(),
+ /* userHandle= */ any()
+ )
+ )
.thenReturn(10)
val user10 = UserHandle.of(/* userId= */ 10)
@@ -615,13 +622,21 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
}
@Test
- fun showNoteTask_copeDevices_tailButtonEntryPoint_shouldStartBubbleInWorkProfile() {
+ fun showNoteTask_copeDevices_tailButtonEntryPoint_shouldStartBubbleInTheUserSelectedUser() {
+ whenever(
+ secureSettings.getIntForUser(
+ /* name= */ eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE),
+ /* def= */ any(),
+ /* userHandle= */ any()
+ )
+ )
+ .thenReturn(mainUserInfo.id)
whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true)
userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo))
createNoteTaskController().showNoteTask(entryPoint = TAIL_BUTTON)
- verifyNoteTaskOpenInBubbleInUser(workUserInfo.userHandle)
+ verifyNoteTaskOpenInBubbleInUser(mainUserInfo.userHandle)
}
@Test
@@ -813,7 +828,15 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
}
@Test
- fun getUserForHandlingNotesTaking_cope_tailButton_shouldReturnWorkProfileUser() {
+ fun getUserForHandlingNotesTaking_cope_userSelectedWorkProfile_tailButton_shouldReturnWorkProfileUser() { // ktlint-disable max-line-length
+ whenever(
+ secureSettings.getIntForUser(
+ /* name= */ eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE),
+ /* def= */ any(),
+ /* userHandle= */ any()
+ )
+ )
+ .thenReturn(workUserInfo.id)
whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true)
userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo))
@@ -823,6 +846,24 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
}
@Test
+ fun getUserForHandlingNotesTaking_cope_userSelectedMainProfile_tailButton_shouldReturnMainProfileUser() { // ktlint-disable max-line-length
+ whenever(
+ secureSettings.getIntForUser(
+ /* name= */ eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE),
+ /* def= */ any(),
+ /* userHandle= */ any()
+ )
+ )
+ .thenReturn(mainUserInfo.id)
+ whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true)
+ userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo))
+
+ val user = createNoteTaskController().getUserForHandlingNotesTaking(TAIL_BUTTON)
+
+ assertThat(user).isEqualTo(UserHandle.of(mainUserInfo.id))
+ }
+
+ @Test
fun getUserForHandlingNotesTaking_cope_appClip_shouldReturnCurrentUser() {
whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true)
userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt
new file mode 100644
index 000000000000..0a8c0ab9817d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt
@@ -0,0 +1,825 @@
+/*
+ * Copyright (C) 2021 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.privacy
+
+import android.app.ActivityManager
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.ResolveInfoFlags
+import android.content.pm.ResolveInfo
+import android.content.pm.UserInfo
+import android.os.Process.SYSTEM_UID
+import android.os.UserHandle
+import android.permission.PermissionGroupUsage
+import android.permission.PermissionManager
+import android.testing.AndroidTestingRunner
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.appops.AppOpsController
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.privacy.logging.PrivacyLogger
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.capture
+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 org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class PrivacyDialogControllerV2Test : SysuiTestCase() {
+
+ companion object {
+ private const val USER_ID = 0
+ private const val ENT_USER_ID = 10
+
+ private const val TEST_PACKAGE_NAME = "test package name"
+ private const val TEST_ATTRIBUTION_TAG = "test attribution tag"
+ private const val TEST_PROXY_LABEL = "test proxy label"
+
+ private const val PERM_CAMERA = android.Manifest.permission_group.CAMERA
+ private const val PERM_MICROPHONE = android.Manifest.permission_group.MICROPHONE
+ private const val PERM_LOCATION = android.Manifest.permission_group.LOCATION
+
+ private val TEST_INTENT = Intent("test_intent_action")
+ }
+
+ @Mock
+ private lateinit var dialog: PrivacyDialogV2
+ @Mock
+ private lateinit var permissionManager: PermissionManager
+ @Mock
+ private lateinit var packageManager: PackageManager
+ @Mock
+ private lateinit var privacyItemController: PrivacyItemController
+ @Mock
+ private lateinit var userTracker: UserTracker
+ @Mock
+ private lateinit var activityStarter: ActivityStarter
+ @Mock
+ private lateinit var privacyLogger: PrivacyLogger
+ @Mock
+ private lateinit var keyguardStateController: KeyguardStateController
+ @Mock
+ private lateinit var appOpsController: AppOpsController
+ @Captor
+ private lateinit var dialogDismissedCaptor: ArgumentCaptor<PrivacyDialogV2.OnDialogDismissed>
+ @Captor
+ private lateinit var activityStartedCaptor: ArgumentCaptor<ActivityStarter.Callback>
+ @Captor
+ private lateinit var intentCaptor: ArgumentCaptor<Intent>
+ @Mock
+ private lateinit var uiEventLogger: UiEventLogger
+ @Mock
+ private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
+
+ private val backgroundExecutor = FakeExecutor(FakeSystemClock())
+ private val uiExecutor = FakeExecutor(FakeSystemClock())
+ private lateinit var controller: PrivacyDialogControllerV2
+ private var nextUid: Int = 0
+
+ private val dialogProvider = object : PrivacyDialogControllerV2.DialogProvider {
+ var list: List<PrivacyDialogV2.PrivacyElement>? = null
+ var manageApp: ((String, Int, Intent) -> Unit)? = null
+ var closeApp: ((String, Int) -> Unit)? = null
+ var openPrivacyDashboard: (() -> Unit)? = null
+
+ override fun makeDialog(
+ context: Context,
+ list: List<PrivacyDialogV2.PrivacyElement>,
+ manageApp: (String, Int, Intent) -> Unit,
+ closeApp: (String, Int) -> Unit,
+ openPrivacyDashboard: () -> Unit
+ ): PrivacyDialogV2 {
+ this.list = list
+ this.manageApp = manageApp
+ this.closeApp = closeApp
+ this.openPrivacyDashboard = openPrivacyDashboard
+ return dialog
+ }
+ }
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ nextUid = 0
+ setUpDefaultMockResponses()
+
+ controller = PrivacyDialogControllerV2(
+ permissionManager,
+ packageManager,
+ privacyItemController,
+ userTracker,
+ activityStarter,
+ backgroundExecutor,
+ uiExecutor,
+ privacyLogger,
+ keyguardStateController,
+ appOpsController,
+ uiEventLogger,
+ dialogLaunchAnimator,
+ dialogProvider
+ )
+ }
+
+ @After
+ fun tearDown() {
+ FakeExecutor.exhaustExecutors(uiExecutor, backgroundExecutor)
+ dialogProvider.list = null
+ dialogProvider.manageApp = null
+ dialogProvider.closeApp = null
+ dialogProvider.openPrivacyDashboard = null
+ }
+
+ @Test
+ fun testMicMutedParameter() {
+ `when`(appOpsController.isMicMuted).thenReturn(true)
+ controller.showDialog(context)
+ backgroundExecutor.runAllReady()
+
+ verify(permissionManager).getIndicatorAppOpUsageData(true)
+ }
+
+ @Test
+ fun testPermissionManagerOnlyCalledInBackgroundThread() {
+ controller.showDialog(context)
+ verify(permissionManager, never()).getIndicatorAppOpUsageData(anyBoolean())
+ backgroundExecutor.runAllReady()
+ verify(permissionManager).getIndicatorAppOpUsageData(anyBoolean())
+ }
+
+ @Test
+ fun testPackageManagerOnlyCalledInBackgroundThread() {
+ val usage = createMockPermGroupUsage()
+ `when`(usage.isPhoneCall).thenReturn(false)
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ verify(packageManager, never()).getApplicationInfoAsUser(anyString(), anyInt(), anyInt())
+ backgroundExecutor.runAllReady()
+ verify(packageManager, atLeastOnce())
+ .getApplicationInfoAsUser(anyString(), anyInt(), anyInt())
+ }
+
+ @Test
+ fun testShowDialogShowsDialogWithoutView() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialogLaunchAnimator, never()).showFromView(any(), any(), any(), anyBoolean())
+ verify(dialog).show()
+ }
+
+ @Test
+ fun testShowDialogShowsDialogWithView() {
+ val view = View(context)
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context, view)
+ exhaustExecutors()
+
+ verify(dialogLaunchAnimator).showFromView(dialog, view)
+ verify(dialog, never()).show()
+ }
+
+ @Test
+ fun testDontShowEmptyDialog() {
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog, never()).show()
+ }
+
+ @Test
+ fun testHideDialogDismissesDialogIfShown() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ controller.dismissDialog()
+ verify(dialog).dismiss()
+ }
+
+ @Test
+ fun testHideDialogNoopIfNotShown() {
+ controller.dismissDialog()
+ verify(dialog, never()).dismiss()
+ }
+
+ @Test
+ fun testHideDialogNoopAfterDismissed() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog).addOnDismissListener(capture(dialogDismissedCaptor))
+
+ dialogDismissedCaptor.value.onDialogDismissed()
+ controller.dismissDialog()
+ verify(dialog, never()).dismiss()
+ }
+
+ @Test
+ fun testShowForAllUsers() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+
+ exhaustExecutors()
+ verify(dialog).setShowForAllUsers(true)
+ }
+
+ @Test
+ fun testSingleElementInList() {
+ val usage = createMockPermGroupUsage(
+ packageName = TEST_PACKAGE_NAME,
+ uid = generateUidForUser(USER_ID),
+ permissionGroupName = PERM_CAMERA,
+ lastAccessTimeMillis = 5L,
+ isActive = true,
+ isPhoneCall = false,
+ attributionTag = null,
+ proxyLabel = TEST_PROXY_LABEL
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(list.get(0).type).isEqualTo(PrivacyType.TYPE_CAMERA)
+ assertThat(list.get(0).packageName).isEqualTo(TEST_PACKAGE_NAME)
+ assertThat(list.get(0).userId).isEqualTo(USER_ID)
+ assertThat(list.get(0).applicationName).isEqualTo(TEST_PACKAGE_NAME)
+ assertThat(list.get(0).attributionTag).isNull()
+ assertThat(list.get(0).attributionLabel).isNull()
+ assertThat(list.get(0).proxyLabel).isEqualTo(TEST_PROXY_LABEL)
+ assertThat(list.get(0).lastActiveTimestamp).isEqualTo(5L)
+ assertThat(list.get(0).isActive).isTrue()
+ assertThat(list.get(0).isPhoneCall).isFalse()
+ assertThat(list.get(0).isService).isFalse()
+ assertThat(list.get(0).permGroupName).isEqualTo(PERM_CAMERA)
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
+ .isTrue()
+ }
+ }
+
+ private fun isIntentEqual(actual: Intent, expected: Intent): Boolean {
+ return actual.action == expected.action &&
+ actual.getStringExtra(Intent.EXTRA_PACKAGE_NAME) ==
+ expected.getStringExtra(Intent.EXTRA_PACKAGE_NAME) &&
+ actual.getParcelableExtra(Intent.EXTRA_USER) as? UserHandle ==
+ expected.getParcelableExtra(Intent.EXTRA_USER) as? UserHandle
+ }
+
+ @Test
+ fun testTwoElementsDifferentType_sorted() {
+ val usage_camera = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_camera",
+ permissionGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_microphone",
+ permissionGroupName = PERM_MICROPHONE
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_microphone, usage_camera)
+ )
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(list).hasSize(2)
+ assertThat(list.get(0).type.compareTo(list.get(1).type)).isLessThan(0)
+ }
+ }
+
+ @Test
+ fun testTwoElementsSameType_oneActive() {
+ val usage_active = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_active",
+ isActive = true
+ )
+ val usage_recent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_recent",
+ isActive = false
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_recent, usage_active)
+ )
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(1)
+ assertThat(dialogProvider.list?.get(0)?.isActive).isTrue()
+ }
+
+ @Test
+ fun testTwoElementsSameType_twoActive() {
+ val usage_active = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_active",
+ isActive = true,
+ lastAccessTimeMillis = 0L
+ )
+ val usage_active_moreRecent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_active_recent",
+ isActive = true,
+ lastAccessTimeMillis = 1L
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_active, usage_active_moreRecent)
+ )
+ controller.showDialog(context)
+ exhaustExecutors()
+ assertThat(dialogProvider.list).hasSize(2)
+ assertThat(dialogProvider.list?.get(0)?.lastActiveTimestamp).isEqualTo(1L)
+ assertThat(dialogProvider.list?.get(1)?.lastActiveTimestamp).isEqualTo(0L)
+ }
+
+ @Test
+ fun testManyElementsSameType_bothRecent() {
+ val usage_recent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_recent",
+ isActive = false,
+ lastAccessTimeMillis = 0L
+ )
+ val usage_moreRecent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_moreRecent",
+ isActive = false,
+ lastAccessTimeMillis = 1L
+ )
+ val usage_mostRecent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_mostRecent",
+ isActive = false,
+ lastAccessTimeMillis = 2L
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_recent, usage_mostRecent, usage_moreRecent)
+ )
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(1)
+ assertThat(dialogProvider.list?.get(0)?.lastActiveTimestamp).isEqualTo(2L)
+ }
+
+ @Test
+ fun testMicAndCameraDisabled() {
+ val usage_camera = createMockPermGroupUsage(
+ permissionGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ permissionGroupName = PERM_MICROPHONE
+ )
+ val usage_location = createMockPermGroupUsage(
+ permissionGroupName = PERM_LOCATION
+ )
+
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_camera, usage_location, usage_microphone)
+ )
+ `when`(privacyItemController.micCameraAvailable).thenReturn(false)
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(1)
+ assertThat(dialogProvider.list?.get(0)?.type).isEqualTo(PrivacyType.TYPE_LOCATION)
+ }
+
+ @Test
+ fun testLocationDisabled() {
+ val usage_camera = createMockPermGroupUsage(
+ permissionGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ permissionGroupName = PERM_MICROPHONE
+ )
+ val usage_location = createMockPermGroupUsage(
+ permissionGroupName = PERM_LOCATION
+ )
+
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_camera, usage_location, usage_microphone)
+ )
+ `when`(privacyItemController.locationAvailable).thenReturn(false)
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(2)
+ dialogProvider.list?.forEach {
+ assertThat(it.type).isNotEqualTo(PrivacyType.TYPE_LOCATION)
+ }
+ }
+
+ @Test
+ fun testAllIndicatorsAvailable() {
+ val usage_camera = createMockPermGroupUsage(
+ permissionGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ permissionGroupName = PERM_MICROPHONE
+ )
+ val usage_location = createMockPermGroupUsage(
+ permissionGroupName = PERM_LOCATION
+ )
+
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_camera, usage_location, usage_microphone)
+ )
+ `when`(privacyItemController.micCameraAvailable).thenReturn(true)
+ `when`(privacyItemController.locationAvailable).thenReturn(true)
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(3)
+ }
+
+ @Test
+ fun testNoIndicatorsAvailable() {
+ val usage_camera = createMockPermGroupUsage(
+ permissionGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ permissionGroupName = PERM_MICROPHONE
+ )
+ val usage_location = createMockPermGroupUsage(
+ permissionGroupName = PERM_LOCATION
+ )
+
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_camera, usage_location, usage_microphone)
+ )
+ `when`(privacyItemController.micCameraAvailable).thenReturn(false)
+ `when`(privacyItemController.locationAvailable).thenReturn(false)
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog, never()).show()
+ }
+
+ @Test
+ fun testNotCurrentUser() {
+ val usage_other = createMockPermGroupUsage(
+ uid = generateUidForUser(ENT_USER_ID + 1)
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean()))
+ .thenReturn(listOf(usage_other))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog, never()).show()
+ }
+
+ @Test
+ fun testStartActivitySuccess() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.manageApp?.invoke(TEST_PACKAGE_NAME, USER_ID, TEST_INTENT)
+ verify(activityStarter).startActivity(any(), eq(true), capture(activityStartedCaptor))
+
+ activityStartedCaptor.value.onActivityStarted(ActivityManager.START_DELIVERED_TO_TOP)
+
+ verify(dialog).dismiss()
+ }
+
+ @Test
+ fun testStartActivityFailure() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.manageApp?.invoke(TEST_PACKAGE_NAME, USER_ID, TEST_INTENT)
+ verify(activityStarter).startActivity(any(), eq(true), capture(activityStartedCaptor))
+
+ activityStartedCaptor.value.onActivityStarted(ActivityManager.START_ABORTED)
+
+ verify(dialog, never()).dismiss()
+ }
+
+ @Test
+ fun testCallOnSecondaryUser() {
+ // Calls happen in
+ val usage = createMockPermGroupUsage(uid = SYSTEM_UID, isPhoneCall = true)
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ `when`(userTracker.userProfiles).thenReturn(listOf(
+ UserInfo(ENT_USER_ID, "", 0)
+ ))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog).show()
+ }
+
+ @Test
+ fun testManageAppLogs() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.manageApp?.invoke(TEST_PACKAGE_NAME, USER_ID, TEST_INTENT)
+ verify(uiEventLogger).log(PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS,
+ USER_ID, TEST_PACKAGE_NAME)
+ }
+
+ @Test
+ fun testCloseAppLogs() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.closeApp?.invoke(TEST_PACKAGE_NAME, USER_ID)
+ verify(uiEventLogger).log(PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_CLOSE_APP,
+ USER_ID, TEST_PACKAGE_NAME)
+ }
+
+ @Test
+ fun testOpenPrivacyDashboardLogs() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.openPrivacyDashboard?.invoke()
+ verify(uiEventLogger).log(PrivacyDialogEvent.PRIVACY_DIALOG_CLICK_TO_PRIVACY_DASHBOARD)
+ }
+
+ @Test
+ fun testDismissedDialogLogs() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog).addOnDismissListener(capture(dialogDismissedCaptor))
+
+ dialogDismissedCaptor.value.onDialogDismissed()
+
+ controller.dismissDialog()
+
+ verify(uiEventLogger, times(1)).log(PrivacyDialogEvent.PRIVACY_DIALOG_DISMISSED)
+ }
+
+ @Test
+ fun testDefaultIntent() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
+ .isTrue()
+ assertThat(list.get(0).isService).isFalse()
+ }
+ }
+
+ @Test
+ fun testDefaultIntentOnEnterpriseUser() {
+ val usage =
+ createMockPermGroupUsage(
+ uid = generateUidForUser(ENT_USER_ID),
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(
+ TEST_PACKAGE_NAME, ENT_USER_ID)))
+ .isTrue()
+ assertThat(list.get(0).isService).isFalse()
+ }
+ }
+
+ @Test
+ fun testDefaultIntentOnInvalidAttributionTag() {
+ val usage = createMockPermGroupUsage(
+ attributionTag = "INVALID_ATTRIBUTION_TAG",
+ proxyLabel = TEST_PROXY_LABEL
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
+ .isTrue()
+ assertThat(list.get(0).isService).isFalse()
+ }
+ }
+
+ @Test
+ fun testServiceIntentOnCorrectSubAttribution() {
+ val usage = createMockPermGroupUsage(
+ attributionTag = TEST_ATTRIBUTION_TAG,
+ attributionLabel = "TEST_LABEL"
+ )
+
+ val activityInfo = createMockActivityInfo()
+ val resolveInfo = createMockResolveInfo(activityInfo)
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ `when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>()))
+ .thenAnswer { resolveInfo }
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ val navigationIntent = list.get(0).navigationIntent!!
+ assertThat(navigationIntent.action).isEqualTo(Intent.ACTION_MANAGE_PERMISSION_USAGE)
+ assertThat(navigationIntent.getStringExtra(Intent.EXTRA_PERMISSION_GROUP_NAME))
+ .isEqualTo(PERM_CAMERA)
+ assertThat(navigationIntent.getStringArrayExtra(Intent.EXTRA_ATTRIBUTION_TAGS))
+ .isEqualTo(arrayOf(TEST_ATTRIBUTION_TAG.toString()))
+ assertThat(navigationIntent.getBooleanExtra(Intent.EXTRA_SHOWING_ATTRIBUTION, false))
+ .isTrue()
+ assertThat(list.get(0).isService).isTrue()
+ }
+ }
+
+ @Test
+ fun testDefaultIntentOnMissingAttributionLabel() {
+ val usage = createMockPermGroupUsage(
+ attributionTag = TEST_ATTRIBUTION_TAG
+ )
+
+ val activityInfo = createMockActivityInfo()
+ val resolveInfo = createMockResolveInfo(activityInfo)
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ `when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>()))
+ .thenAnswer { resolveInfo }
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
+ .isTrue()
+ assertThat(list.get(0).isService).isFalse()
+ }
+ }
+
+ @Test
+ fun testDefaultIntentOnIncorrectPermission() {
+ val usage = createMockPermGroupUsage(
+ attributionTag = TEST_ATTRIBUTION_TAG
+ )
+
+ val activityInfo = createMockActivityInfo(
+ permission = "INCORRECT_PERMISSION"
+ )
+ val resolveInfo = createMockResolveInfo(activityInfo)
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ `when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>()))
+ .thenAnswer { resolveInfo }
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
+ .isTrue()
+ assertThat(list.get(0).isService).isFalse()
+ }
+ }
+
+ private fun exhaustExecutors() {
+ FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor)
+ }
+
+ private fun setUpDefaultMockResponses() {
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(emptyList())
+ `when`(appOpsController.isMicMuted).thenReturn(false)
+
+ `when`(packageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+ .thenAnswer { FakeApplicationInfo(it.getArgument(0)) }
+
+ `when`(privacyItemController.locationAvailable).thenReturn(true)
+ `when`(privacyItemController.micCameraAvailable).thenReturn(true)
+
+ `when`(userTracker.userProfiles).thenReturn(listOf(
+ UserInfo(USER_ID, "", 0),
+ UserInfo(ENT_USER_ID, "", UserInfo.FLAG_MANAGED_PROFILE)
+ ))
+
+ `when`(keyguardStateController.isUnlocked).thenReturn(true)
+ }
+
+ private class FakeApplicationInfo(val label: CharSequence) : ApplicationInfo() {
+ override fun loadLabel(pm: PackageManager): CharSequence {
+ return label
+ }
+ }
+
+ private fun generateUidForUser(user: Int): Int {
+ return user * UserHandle.PER_USER_RANGE + nextUid++
+ }
+
+ private fun createMockResolveInfo(
+ activityInfo: ActivityInfo? = null
+ ): ResolveInfo {
+ val resolveInfo = mock(ResolveInfo::class.java)
+ resolveInfo.activityInfo = activityInfo
+ return resolveInfo
+ }
+
+ private fun createMockActivityInfo(
+ permission: String = android.Manifest.permission.START_VIEW_PERMISSION_USAGE,
+ className: String = "TEST_CLASS_NAME"
+ ): ActivityInfo {
+ val activityInfo = mock(ActivityInfo::class.java)
+ activityInfo.permission = permission
+ activityInfo.name = className
+ return activityInfo
+ }
+
+ private fun createMockPermGroupUsage(
+ packageName: String = TEST_PACKAGE_NAME,
+ uid: Int = generateUidForUser(USER_ID),
+ permissionGroupName: String = PERM_CAMERA,
+ lastAccessTimeMillis: Long = 0L,
+ isActive: Boolean = false,
+ isPhoneCall: Boolean = false,
+ attributionTag: CharSequence? = null,
+ attributionLabel: CharSequence? = null,
+ proxyLabel: CharSequence? = null
+ ): PermissionGroupUsage {
+ val usage = mock(PermissionGroupUsage::class.java)
+ `when`(usage.packageName).thenReturn(packageName)
+ `when`(usage.uid).thenReturn(uid)
+ `when`(usage.permissionGroupName).thenReturn(permissionGroupName)
+ `when`(usage.lastAccessTimeMillis).thenReturn(lastAccessTimeMillis)
+ `when`(usage.isActive).thenReturn(isActive)
+ `when`(usage.isPhoneCall).thenReturn(isPhoneCall)
+ `when`(usage.attributionTag).thenReturn(attributionTag)
+ `when`(usage.attributionLabel).thenReturn(attributionLabel)
+ `when`(usage.proxyLabel).thenReturn(proxyLabel)
+ return usage
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogV2Test.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogV2Test.kt
new file mode 100644
index 000000000000..f4644a578d24
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogV2Test.kt
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2021 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.privacy
+
+import android.content.Intent
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.widget.TextView
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class PrivacyDialogV2Test : SysuiTestCase() {
+
+ companion object {
+ private const val TEST_PACKAGE_NAME = "test_pkg"
+ private const val TEST_USER_ID = 0
+ private const val TEST_PERM_GROUP = "test_perm_group"
+
+ private val TEST_INTENT = Intent("test_intent_action")
+
+ private fun createPrivacyElement(
+ type: PrivacyType = PrivacyType.TYPE_MICROPHONE,
+ packageName: String = TEST_PACKAGE_NAME,
+ userId: Int = TEST_USER_ID,
+ applicationName: CharSequence = "App",
+ attributionTag: CharSequence? = null,
+ attributionLabel: CharSequence? = null,
+ proxyLabel: CharSequence? = null,
+ lastActiveTimestamp: Long = 0L,
+ isActive: Boolean = false,
+ isPhoneCall: Boolean = false,
+ isService: Boolean = false,
+ permGroupName: String = TEST_PERM_GROUP,
+ navigationIntent: Intent = TEST_INTENT
+ ) =
+ PrivacyDialogV2.PrivacyElement(
+ type,
+ packageName,
+ userId,
+ applicationName,
+ attributionTag,
+ attributionLabel,
+ proxyLabel,
+ lastActiveTimestamp,
+ isActive,
+ isPhoneCall,
+ isService,
+ permGroupName,
+ navigationIntent
+ )
+ }
+
+ @Mock private lateinit var manageApp: (String, Int, Intent) -> Unit
+ @Mock private lateinit var closeApp: (String, Int) -> Unit
+ @Mock private lateinit var openPrivacyDashboard: () -> Unit
+ private lateinit var dialog: PrivacyDialogV2
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @After
+ fun teardown() {
+ if (this::dialog.isInitialized) {
+ dialog.dismiss()
+ }
+ }
+
+ @Test
+ fun testManageAppCalledWithCorrectParams() {
+ val list = listOf(createPrivacyElement())
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+ dialog.show()
+
+ dialog.requireViewById<View>(R.id.privacy_dialog_manage_app_button).callOnClick()
+
+ verify(manageApp).invoke(TEST_PACKAGE_NAME, TEST_USER_ID, TEST_INTENT)
+ }
+
+ @Test
+ fun testCloseAppCalledWithCorrectParams() {
+ val list = listOf(createPrivacyElement(isActive = true))
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+ dialog.show()
+
+ dialog.requireViewById<View>(R.id.privacy_dialog_close_app_button).callOnClick()
+
+ verify(closeApp).invoke(TEST_PACKAGE_NAME, TEST_USER_ID)
+ }
+
+ @Test
+ fun testCloseAppMissingForService() {
+ val list = listOf(createPrivacyElement(isActive = true, isService = true))
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.findViewById<View>(R.id.privacy_dialog_manage_app_button)).isNotNull()
+ assertThat(dialog.findViewById<View>(R.id.privacy_dialog_close_app_button)).isNull()
+ }
+
+ @Test
+ fun testMoreButton() {
+ dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
+ dialog.show()
+
+ dialog.requireViewById<View>(R.id.privacy_dialog_more_button).callOnClick()
+
+ verify(openPrivacyDashboard).invoke()
+ }
+
+ @Test
+ fun testCloseButton() {
+ dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
+ val dismissListener = mock(PrivacyDialogV2.OnDialogDismissed::class.java)
+ dialog.addOnDismissListener(dismissListener)
+ dialog.show()
+ verify(dismissListener, never()).onDialogDismissed()
+
+ dialog.requireViewById<View>(R.id.privacy_dialog_close_button).callOnClick()
+
+ verify(dismissListener).onDialogDismissed()
+ }
+
+ @Test
+ fun testDismissListenerCalledOnDismiss() {
+ dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
+ val dismissListener = mock(PrivacyDialogV2.OnDialogDismissed::class.java)
+ dialog.addOnDismissListener(dismissListener)
+ dialog.show()
+ verify(dismissListener, never()).onDialogDismissed()
+
+ dialog.dismiss()
+
+ verify(dismissListener).onDialogDismissed()
+ }
+
+ @Test
+ fun testDismissListenerCalledImmediatelyIfDialogAlreadyDismissed() {
+ dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
+ val dismissListener = mock(PrivacyDialogV2.OnDialogDismissed::class.java)
+ dialog.show()
+ dialog.dismiss()
+
+ dialog.addOnDismissListener(dismissListener)
+
+ verify(dismissListener).onDialogDismissed()
+ }
+
+ @Test
+ fun testCorrectNumElements() {
+ val list =
+ listOf(
+ createPrivacyElement(type = PrivacyType.TYPE_CAMERA, isActive = true),
+ createPrivacyElement()
+ )
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(
+ dialog.requireViewById<ViewGroup>(R.id.privacy_dialog_items_container).childCount
+ )
+ .isEqualTo(2)
+ }
+
+ @Test
+ fun testHeaderText() {
+ val list = listOf(createPrivacyElement())
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_title).text)
+ .isEqualTo(TEST_PERM_GROUP)
+ }
+
+ @Test
+ fun testUsingText() {
+ val list = listOf(createPrivacyElement(type = PrivacyType.TYPE_CAMERA, isActive = true))
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
+ .isEqualTo("In use by App")
+ }
+
+ @Test
+ fun testRecentText() {
+ val list = listOf(createPrivacyElement())
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
+ .isEqualTo("Recently used by App")
+ }
+
+ @Test
+ fun testPhoneCall() {
+ val list = listOf(createPrivacyElement(isPhoneCall = true))
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
+ .isEqualTo("Recently used in phone call")
+ }
+
+ @Test
+ fun testPhoneCallNotClickable() {
+ val list = listOf(createPrivacyElement(isPhoneCall = true))
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<View>(R.id.privacy_dialog_item_card).isClickable)
+ .isFalse()
+ assertThat(
+ dialog
+ .requireViewById<View>(R.id.privacy_dialog_item_header_expand_toggle)
+ .visibility
+ )
+ .isEqualTo(View.GONE)
+ }
+
+ @Test
+ fun testProxyLabel() {
+ val list = listOf(createPrivacyElement(proxyLabel = "proxy label"))
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
+ .isEqualTo("Recently used by App (proxy label)")
+ }
+
+ @Test
+ fun testSubattribution() {
+ val list =
+ listOf(
+ createPrivacyElement(
+ attributionLabel = "For subattribution",
+ isActive = true,
+ isService = true
+ )
+ )
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
+ .isEqualTo("In use by App (For subattribution)")
+ }
+
+ @Test
+ fun testSubattributionAndProxyLabel() {
+ val list =
+ listOf(
+ createPrivacyElement(
+ attributionLabel = "For subattribution",
+ proxyLabel = "proxy label",
+ isActive = true
+ )
+ )
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+ dialog.show()
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
+ .isEqualTo("In use by App (For subattribution \u2022 proxy label)")
+ }
+
+ @Test
+ fun testDialogHasTitle() {
+ val list = listOf(createPrivacyElement())
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+ dialog.show()
+
+ assertThat(dialog.window?.attributes?.title).isEqualTo("Microphone & Camera")
+ }
+
+ @Test
+ fun testDialogIsFullscreen() {
+ dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
+ dialog.show()
+
+ assertThat(dialog.window?.attributes?.width).isEqualTo(MATCH_PARENT)
+ assertThat(dialog.window?.attributes?.height).isEqualTo(MATCH_PARENT)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
index 3620233fc9df..fa02e8cb3e54 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
@@ -13,9 +13,12 @@ import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.appops.AppOpsController
import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyDialogController
+import com.android.systemui.privacy.PrivacyDialogControllerV2
import com.android.systemui.privacy.PrivacyItemController
import com.android.systemui.privacy.logging.PrivacyLogger
import com.android.systemui.statusbar.phone.StatusIconContainer
@@ -24,6 +27,7 @@ import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.time.FakeSystemClock
import org.junit.Before
@@ -54,6 +58,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
@Mock
private lateinit var privacyDialogController: PrivacyDialogController
@Mock
+ private lateinit var privacyDialogControllerV2: PrivacyDialogControllerV2
+ @Mock
private lateinit var privacyLogger: PrivacyLogger
@Mock
private lateinit var iconContainer: StatusIconContainer
@@ -69,6 +75,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
private lateinit var safetyCenterManager: SafetyCenterManager
@Mock
private lateinit var deviceProvisionedController: DeviceProvisionedController
+ @Mock
+ private lateinit var featureFlags: FeatureFlags
private val uiExecutor = FakeExecutor(FakeSystemClock())
private val backgroundExecutor = FakeExecutor(FakeSystemClock())
@@ -94,6 +102,7 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
uiEventLogger,
privacyChip,
privacyDialogController,
+ privacyDialogControllerV2,
privacyLogger,
iconContainer,
permissionManager,
@@ -103,7 +112,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
appOpsController,
broadcastDispatcher,
safetyCenterManager,
- deviceProvisionedController
+ deviceProvisionedController,
+ featureFlags
)
backgroundExecutor.runAllReady()
@@ -154,17 +164,55 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
}
@Test
- fun testPrivacyChipClicked() {
+ fun testPrivacyChipClickedWhenNewDialogDisabledAndSafetyCenterDisabled() {
+ whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(false)
+ whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(false)
+ controller.onParentVisible()
+ val captor = argumentCaptor<View.OnClickListener>()
+ verify(privacyChip).setOnClickListener(capture(captor))
+ captor.value.onClick(privacyChip)
+ verify(privacyDialogController).showDialog(any(Context::class.java))
+ verify(privacyDialogControllerV2, never())
+ .showDialog(any(Context::class.java), any(View::class.java))
+ }
+
+ @Test
+ fun testPrivacyChipClickedWhenNewDialogEnabledAndSafetyCenterDisabled() {
+ whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(true)
whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(false)
controller.onParentVisible()
val captor = argumentCaptor<View.OnClickListener>()
verify(privacyChip).setOnClickListener(capture(captor))
captor.value.onClick(privacyChip)
verify(privacyDialogController).showDialog(any(Context::class.java))
+ verify(privacyDialogControllerV2, never())
+ .showDialog(any(Context::class.java), any(View::class.java))
+ }
+
+ @Test
+ fun testPrivacyChipClickedWhenNewDialogDisabledAndSafetyCenterEnabled() {
+ whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(false)
+ val receiverCaptor = argumentCaptor<BroadcastReceiver>()
+ whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(true)
+ verify(broadcastDispatcher).registerReceiver(capture(receiverCaptor),
+ any(), any(), nullable(), anyInt(), nullable())
+ receiverCaptor.value.onReceive(
+ context,
+ Intent(SafetyCenterManager.ACTION_SAFETY_CENTER_ENABLED_CHANGED)
+ )
+ backgroundExecutor.runAllReady()
+ controller.onParentVisible()
+ val captor = argumentCaptor<View.OnClickListener>()
+ verify(privacyChip).setOnClickListener(capture(captor))
+ captor.value.onClick(privacyChip)
+ verify(privacyDialogController, never()).showDialog(any(Context::class.java))
+ verify(privacyDialogControllerV2, never())
+ .showDialog(any(Context::class.java), any(View::class.java))
}
@Test
- fun testSafetyCenterFlag() {
+ fun testPrivacyChipClickedWhenNewDialogEnabledAndSafetyCenterEnabled() {
+ whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(true)
val receiverCaptor = argumentCaptor<BroadcastReceiver>()
whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(true)
verify(broadcastDispatcher).registerReceiver(capture(receiverCaptor),
@@ -178,6 +226,7 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
val captor = argumentCaptor<View.OnClickListener>()
verify(privacyChip).setOnClickListener(capture(captor))
captor.value.onClick(privacyChip)
+ verify(privacyDialogControllerV2).showDialog(any(Context::class.java), eq(privacyChip))
verify(privacyDialogController, never()).showDialog(any(Context::class.java))
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
index 6bb13eacc135..49ece66e0cfd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
@@ -29,6 +29,7 @@ import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
import com.android.systemui.keyguard.ScreenLifecycle
import com.android.systemui.keyguard.WakefulnessLifecycle
@@ -49,11 +50,11 @@ import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.android.wm.shell.sysui.ShellInterface
import com.google.common.util.concurrent.MoreExecutors
-import dagger.Lazy
import java.util.Optional
import java.util.concurrent.Executor
import org.junit.After
@@ -82,6 +83,7 @@ class OverviewProxyServiceTest : SysuiTestCase() {
private val displayTracker = FakeDisplayTracker(mContext)
private val fakeSystemClock = FakeSystemClock()
private val sysUiState = SysUiState(displayTracker)
+ private val featureFlags = FakeFeatureFlags()
private val screenLifecycle = ScreenLifecycle(dumpManager)
private val wakefulnessLifecycle =
WakefulnessLifecycle(mContext, null, fakeSystemClock, dumpManager)
@@ -132,12 +134,13 @@ class OverviewProxyServiceTest : SysuiTestCase() {
executor,
commandQueue,
shellInterface,
- Lazy { navBarController },
- Lazy { Optional.of(centralSurfaces) },
- Lazy { shadeViewController },
+ { navBarController },
+ { Optional.of(centralSurfaces) },
+ { shadeViewController },
navModeController,
statusBarWinController,
sysUiState,
+ mock(),
userTracker,
screenLifecycle,
wakefulnessLifecycle,
@@ -145,6 +148,7 @@ class OverviewProxyServiceTest : SysuiTestCase() {
displayTracker,
sysuiUnlockAnimationController,
assistUtils,
+ featureFlags,
dumpManager,
unfoldTransitionProgressForwarder
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index 3050c4edd24f..d2bbfa85604b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -100,4 +100,15 @@ class SceneInteractorTest : SysuiTestCase() {
)
)
}
+
+ @Test
+ fun remoteUserInput() = runTest {
+ val remoteUserInput by collectLastValue(underTest.remoteUserInput)
+ assertThat(remoteUserInput).isNull()
+
+ for (input in SceneTestUtils.REMOTE_INPUT_DOWN_GESTURE) {
+ underTest.onRemoteUserInput(input)
+ assertThat(remoteUserInput).isEqualTo(input)
+ }
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index 6882be7fe184..63ea918c904a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -18,14 +18,19 @@
package com.android.systemui.scene.ui.viewmodel
+import android.view.MotionEvent
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.scene.SceneTestUtils
+import com.android.systemui.scene.shared.model.RemoteUserInput
+import com.android.systemui.scene.shared.model.RemoteUserInputAction
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.currentTime
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@@ -68,4 +73,35 @@ class SceneContainerViewModelTest : SysuiTestCase() {
underTest.setCurrentScene(SceneModel(SceneKey.Shade))
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Shade))
}
+
+ @Test
+ fun onRemoteUserInput() = runTest {
+ val remoteUserInput by collectLastValue(underTest.remoteUserInput)
+ assertThat(remoteUserInput).isNull()
+
+ val inputs =
+ SceneTestUtils.REMOTE_INPUT_DOWN_GESTURE.map { remoteUserInputToMotionEvent(it) }
+
+ inputs.forEachIndexed { index, input ->
+ underTest.onRemoteUserInput(input)
+ assertThat(remoteUserInput).isEqualTo(SceneTestUtils.REMOTE_INPUT_DOWN_GESTURE[index])
+ }
+ }
+
+ private fun TestScope.remoteUserInputToMotionEvent(input: RemoteUserInput): MotionEvent {
+ return MotionEvent.obtain(
+ currentTime,
+ currentTime,
+ when (input.action) {
+ RemoteUserInputAction.DOWN -> MotionEvent.ACTION_DOWN
+ RemoteUserInputAction.MOVE -> MotionEvent.ACTION_MOVE
+ RemoteUserInputAction.UP -> MotionEvent.ACTION_UP
+ RemoteUserInputAction.CANCEL -> MotionEvent.ACTION_CANCEL
+ RemoteUserInputAction.UNKNOWN -> MotionEvent.ACTION_OUTSIDE
+ },
+ input.x,
+ input.y,
+ 0
+ )
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
index 0c046e93ee20..c68095ca65a7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
@@ -16,17 +16,23 @@
package com.android.systemui.shade
+import android.os.VibrationEffect
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
+import android.view.HapticFeedbackConstants
import android.view.View
import android.view.ViewStub
import androidx.test.filters.SmallTest
import com.android.internal.util.CollectionUtils
import com.android.keyguard.KeyguardClockSwitch.LARGE
import com.android.systemui.R
+import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
import com.android.systemui.statusbar.StatusBarState.KEYGUARD
import com.android.systemui.statusbar.StatusBarState.SHADE
import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
+import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
@@ -55,6 +61,9 @@ class NotificationPanelViewControllerWithCoroutinesTest :
override fun getMainDispatcher() = Dispatchers.Main.immediate
+ private val ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT =
+ VibrationEffect.get(VibrationEffect.EFFECT_STRENGTH_MEDIUM, false)
+
@Test
fun testDisableUserSwitcherAfterEnabling_returnsViewStubToTheViewHierarchy() = runTest {
launch(Dispatchers.Main.immediate) { givenViewAttached() }
@@ -148,6 +157,43 @@ class NotificationPanelViewControllerWithCoroutinesTest :
}
@Test
+ fun doubleTapRequired_onKeyguard_oneWayHapticsDisabled_usesOldVibrate() = runTest {
+ launch(Dispatchers.Main.immediate) {
+ whenever(mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)).thenReturn(false)
+ val listener = getFalsingTapListener()
+ mStatusBarStateController.setState(KEYGUARD)
+
+ listener.onAdditionalTapRequired()
+ val packageName = mView.context.packageName
+ verify(mKeyguardIndicationController).showTransientIndication(anyInt())
+ verify(mVibratorHelper)
+ .vibrate(
+ any(),
+ eq(packageName),
+ eq(ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT),
+ eq("falsing-additional-tap-required"),
+ eq(VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES)
+ )
+ }
+ advanceUntilIdle()
+ }
+
+ @Test
+ fun doubleTapRequired_onKeyguard_oneWayHapticsEnabled_usesPerformHapticFeedback() = runTest {
+ launch(Dispatchers.Main.immediate) {
+ whenever(mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)).thenReturn(true)
+ val listener = getFalsingTapListener()
+ mStatusBarStateController.setState(KEYGUARD)
+
+ listener.onAdditionalTapRequired()
+ verify(mKeyguardIndicationController).showTransientIndication(anyInt())
+ verify(mVibratorHelper)
+ .performHapticFeedback(eq(mView), eq(HapticFeedbackConstants.REJECT))
+ }
+ advanceUntilIdle()
+ }
+
+ @Test
fun testDoubleTapRequired_ShadeLocked() = runTest {
launch(Dispatchers.Main.immediate) {
val listener = getFalsingTapListener()
@@ -161,6 +207,45 @@ class NotificationPanelViewControllerWithCoroutinesTest :
}
@Test
+ fun doubleTapRequired_shadeLocked_oneWayHapticsDisabled_usesOldVibrate() = runTest {
+ launch(Dispatchers.Main.immediate) {
+ whenever(mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)).thenReturn(false)
+ val listener = getFalsingTapListener()
+ val packageName = mView.context.packageName
+ mStatusBarStateController.setState(SHADE_LOCKED)
+
+ listener.onAdditionalTapRequired()
+ verify(mVibratorHelper)
+ .vibrate(
+ any(),
+ eq(packageName),
+ eq(ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT),
+ eq("falsing-additional-tap-required"),
+ eq(VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES)
+ )
+
+ verify(mTapAgainViewController).show()
+ }
+ advanceUntilIdle()
+ }
+
+ @Test
+ fun doubleTapRequired_shadeLocked_oneWayHapticsEnabled_usesPerformHapticFeedback() = runTest {
+ launch(Dispatchers.Main.immediate) {
+ whenever(mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)).thenReturn(true)
+ val listener = getFalsingTapListener()
+ mStatusBarStateController.setState(SHADE_LOCKED)
+
+ listener.onAdditionalTapRequired()
+ verify(mVibratorHelper)
+ .performHapticFeedback(eq(mView), eq(HapticFeedbackConstants.REJECT))
+
+ verify(mTapAgainViewController).show()
+ }
+ advanceUntilIdle()
+ }
+
+ @Test
fun testOnAttachRefreshStatusBarState() = runTest {
launch(Dispatchers.Main.immediate) {
mStatusBarStateController.setState(KEYGUARD)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
index 1643e174ee13..b04d5d3d44e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -517,6 +517,21 @@ public class CommandQueueTest extends SysuiTestCase {
}
@Test
+ public void testConfirmImmersivePrompt() {
+ mCommandQueue.confirmImmersivePrompt();
+ waitForIdleSync();
+ verify(mCallbacks).confirmImmersivePrompt();
+ }
+
+ @Test
+ public void testImmersiveModeChanged() {
+ final int displayAreaId = 10;
+ mCommandQueue.immersiveModeChanged(displayAreaId, true);
+ waitForIdleSync();
+ verify(mCallbacks).immersiveModeChanged(displayAreaId, true);
+ }
+
+ @Test
public void testShowRearDisplayDialog() {
final int currentBaseState = 1;
mCommandQueue.showRearDisplayDialog(currentBaseState);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
index 55b6be9679f2..0b2da8bfa649 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
@@ -18,6 +18,8 @@ package com.android.systemui.statusbar.events
import android.content.Context
import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
import android.util.Pair
import android.view.Gravity
import android.view.View
@@ -37,11 +39,14 @@ import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
+import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
class SystemEventChipAnimationControllerTest : SysuiTestCase() {
private lateinit var controller: SystemEventChipAnimationController
@@ -159,7 +164,7 @@ class SystemEventChipAnimationControllerTest : SysuiTestCase() {
assertThat(chipRect).isEqualTo(Rect(890, 25, 990, 75))
}
- class TestView(context: Context) : View(context), BackgroundAnimatableView {
+ private class TestView(context: Context) : View(context), BackgroundAnimatableView {
override val view: View
get() = this
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
new file mode 100644
index 000000000000..4a94dc819a9e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2023 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.collection.render
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.util.mockito.mock
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+class GroupExpansionManagerTest : SysuiTestCase() {
+ private lateinit var gem: GroupExpansionManagerImpl
+
+ private val dumpManager: DumpManager = mock()
+ private val groupMembershipManager: GroupMembershipManager = mock()
+ private val featureFlags = FakeFeatureFlags()
+
+ private val entry1 = NotificationEntryBuilder().build()
+ private val entry2 = NotificationEntryBuilder().build()
+
+ @Before
+ fun setUp() {
+ whenever(groupMembershipManager.getGroupSummary(entry1)).thenReturn(entry1)
+ whenever(groupMembershipManager.getGroupSummary(entry2)).thenReturn(entry2)
+
+ gem = GroupExpansionManagerImpl(dumpManager, groupMembershipManager, featureFlags)
+ }
+
+ @Test
+ fun testNotifyOnlyOnChange_enabled() {
+ featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
+
+ var listenerCalledCount = 0
+ gem.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ }
+
+ gem.setGroupExpanded(entry1, false)
+ Assert.assertEquals(0, listenerCalledCount)
+ gem.setGroupExpanded(entry1, true)
+ Assert.assertEquals(1, listenerCalledCount)
+ gem.setGroupExpanded(entry2, true)
+ Assert.assertEquals(2, listenerCalledCount)
+ gem.setGroupExpanded(entry1, true)
+ Assert.assertEquals(2, listenerCalledCount)
+ gem.setGroupExpanded(entry2, false)
+ Assert.assertEquals(3, listenerCalledCount)
+ }
+
+ @Test
+ fun testNotifyOnlyOnChange_disabled() {
+ featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, false)
+
+ var listenerCalledCount = 0
+ gem.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ }
+
+ gem.setGroupExpanded(entry1, false)
+ Assert.assertEquals(1, listenerCalledCount)
+ gem.setGroupExpanded(entry1, true)
+ Assert.assertEquals(2, listenerCalledCount)
+ gem.setGroupExpanded(entry2, true)
+ Assert.assertEquals(3, listenerCalledCount)
+ gem.setGroupExpanded(entry1, true)
+ Assert.assertEquals(4, listenerCalledCount)
+ gem.setGroupExpanded(entry2, false)
+ Assert.assertEquals(5, listenerCalledCount)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt
index 2e68cec1fe63..4d4d319a3540 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt
@@ -17,6 +17,10 @@
package com.android.systemui.statusbar.notification.row
+import android.app.Notification
+import android.net.Uri
+import android.os.UserHandle
+import android.os.UserHandle.USER_ALL
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
@@ -28,13 +32,17 @@ import com.android.systemui.flags.FeatureFlags
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.PluginManager
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.SbnBuilder
import com.android.systemui.statusbar.SmartReplyController
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider
import com.android.systemui.statusbar.notification.collection.render.FakeNodeController
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager
import com.android.systemui.statusbar.notification.logging.NotificationLogger
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController.BUBBLES_SETTING_URI
import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer
import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
@@ -45,9 +53,9 @@ import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.time.SystemClock
import com.android.systemui.wmshell.BubblesManager
-import java.util.Optional
import junit.framework.Assert
import org.junit.After
import org.junit.Before
@@ -55,7 +63,10 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.mock
import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import java.util.*
import org.mockito.Mockito.`when` as whenever
@SmallTest
@@ -92,10 +103,10 @@ class ExpandableNotificationRowControllerTest : SysuiTestCase() {
private val featureFlags: FeatureFlags = mock()
private val peopleNotificationIdentifier: PeopleNotificationIdentifier = mock()
private val bubblesManager: BubblesManager = mock()
+ private val settingsController: NotificationSettingsController = mock()
private val dragController: ExpandableNotificationRowDragController = mock()
private val dismissibilityProvider: NotificationDismissibilityProvider = mock()
private val statusBarService: IStatusBarService = mock()
-
private lateinit var controller: ExpandableNotificationRowController
@Before
@@ -132,11 +143,16 @@ class ExpandableNotificationRowControllerTest : SysuiTestCase() {
featureFlags,
peopleNotificationIdentifier,
Optional.of(bubblesManager),
+ settingsController,
dragController,
dismissibilityProvider,
statusBarService
)
whenever(view.childrenContainer).thenReturn(childrenContainer)
+
+ val notification = Notification.Builder(mContext).build()
+ val sbn = SbnBuilder().setNotification(notification).build()
+ whenever(view.entry).thenReturn(NotificationEntryBuilder().setSbn(sbn).build())
}
@After
@@ -204,4 +220,74 @@ class ExpandableNotificationRowControllerTest : SysuiTestCase() {
Mockito.verify(view).removeChildNotification(eq(childView))
Mockito.verify(listContainer).notifyGroupChildRemoved(eq(childView), eq(childrenContainer))
}
+
+ @Test
+ fun registerSettingsListener_forBubbles() {
+ controller.init(mock(NotificationEntry::class.java))
+ val viewStateObserver = withArgCaptor {
+ verify(view).addOnAttachStateChangeListener(capture());
+ }
+ viewStateObserver.onViewAttachedToWindow(view);
+ verify(settingsController).addCallback(any(), any());
+ }
+
+ @Test
+ fun unregisterSettingsListener_forBubbles() {
+ controller.init(mock(NotificationEntry::class.java))
+ val viewStateObserver = withArgCaptor {
+ verify(view).addOnAttachStateChangeListener(capture());
+ }
+ viewStateObserver.onViewDetachedFromWindow(view);
+ verify(settingsController).removeCallback(any(), any());
+ }
+
+ @Test
+ fun settingsListener_invalidUri() {
+ controller.mSettingsListener.onSettingChanged(Uri.EMPTY, view.entry.sbn.userId, "1")
+
+ verify(view, never()).getPrivateLayout()
+ }
+
+ @Test
+ fun settingsListener_invalidUserId() {
+ controller.mSettingsListener.onSettingChanged(BUBBLES_SETTING_URI, -1000, "1")
+ controller.mSettingsListener.onSettingChanged(BUBBLES_SETTING_URI, -1000, null)
+
+ verify(view, never()).getPrivateLayout()
+ }
+
+ @Test
+ fun settingsListener_validUserId() {
+ val childView: NotificationContentView = mock()
+ whenever(view.privateLayout).thenReturn(childView)
+
+ controller.mSettingsListener.onSettingChanged(
+ BUBBLES_SETTING_URI, view.entry.sbn.userId, "1")
+ verify(childView).setBubblesEnabledForUser(true)
+
+ controller.mSettingsListener.onSettingChanged(
+ BUBBLES_SETTING_URI, view.entry.sbn.userId, "9")
+ verify(childView).setBubblesEnabledForUser(false)
+ }
+
+ @Test
+ fun settingsListener_userAll() {
+ val childView: NotificationContentView = mock()
+ whenever(view.privateLayout).thenReturn(childView)
+
+ val notification = Notification.Builder(mContext).build()
+ val sbn = SbnBuilder().setNotification(notification)
+ .setUser(UserHandle.of(USER_ALL))
+ .build()
+ whenever(view.entry).thenReturn(NotificationEntryBuilder()
+ .setSbn(sbn)
+ .setUser(UserHandle.of(USER_ALL))
+ .build())
+
+ controller.mSettingsListener.onSettingChanged(BUBBLES_SETTING_URI, 9, "1")
+ verify(childView).setBubblesEnabledForUser(true)
+
+ controller.mSettingsListener.onSettingChanged(BUBBLES_SETTING_URI, 1, "0")
+ verify(childView).setBubblesEnabledForUser(false)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
index 0b90ebec3ec6..ba6c7fd50bc5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
@@ -250,6 +250,9 @@ class NotificationContentViewTest : SysuiTestCase() {
.thenReturn(actionListMarginTarget)
view.setContainingNotification(mockContainingNotification)
+ // Given: controller says bubbles are enabled for the user
+ view.setBubblesEnabledForUser(true);
+
// When: call NotificationContentView.setExpandedChild() to set the expandedChild
view.expandedChild = mockExpandedChild
@@ -301,6 +304,9 @@ class NotificationContentViewTest : SysuiTestCase() {
view.expandedChild = mockExpandedChild
assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
+ // Given: controller says bubbles are enabled for the user
+ view.setBubblesEnabledForUser(true);
+
// When: call NotificationContentView.onNotificationUpdated() to update the
// NotificationEntry, which should show bubble button
view.onNotificationUpdated(createMockNotificationEntry(true))
@@ -405,7 +411,6 @@ class NotificationContentViewTest : SysuiTestCase() {
val userMock: UserHandle = mock()
whenever(this.sbn).thenReturn(sbnMock)
whenever(sbnMock.user).thenReturn(userMock)
- doReturn(showButton).whenever(view).shouldShowBubbleButton(this)
}
private fun createLinearLayoutWithBottomMargin(bottomMargin: Int): LinearLayout {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSettingsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSettingsControllerTest.kt
new file mode 100644
index 000000000000..2bccdcafbb6e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSettingsControllerTest.kt
@@ -0,0 +1,245 @@
+/*
+ * Copyright (c) 2023 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.row
+
+import android.app.ActivityManager
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.provider.Settings.Secure
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.notification.row.NotificationSettingsController.Listener
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.SecureSettings
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class NotificationSettingsControllerTest : SysuiTestCase() {
+
+ val setting1: String = Secure.NOTIFICATION_BUBBLES
+ val setting2: String = Secure.ACCESSIBILITY_ENABLED
+ val settingUri1: Uri = Secure.getUriFor(setting1)
+ val settingUri2: Uri = Secure.getUriFor(setting2)
+
+ @Mock
+ private lateinit var userTracker: UserTracker
+ private lateinit var handler: Handler
+ private lateinit var testableLooper: TestableLooper
+ @Mock
+ private lateinit var secureSettings: SecureSettings
+ @Mock
+ private lateinit var dumpManager: DumpManager
+
+ @Captor
+ private lateinit var userTrackerCallbackCaptor: ArgumentCaptor<UserTracker.Callback>
+ @Captor
+ private lateinit var settingsObserverCaptor: ArgumentCaptor<ContentObserver>
+
+ private lateinit var controller: NotificationSettingsController
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ testableLooper = TestableLooper.get(this)
+ handler = Handler(testableLooper.looper)
+ allowTestableLooperAsMainThread()
+ controller =
+ NotificationSettingsController(
+ userTracker,
+ handler,
+ secureSettings,
+ dumpManager
+ )
+ }
+
+ @After
+ fun tearDown() {
+ disallowTestableLooperAsMainThread()
+ }
+
+ @Test
+ fun creationRegistersCallbacks() {
+ verify(userTracker).addCallback(any(), any())
+ verify(dumpManager).registerNormalDumpable(anyString(), eq(controller))
+ }
+ @Test
+ fun updateContentObserverRegistration_onUserChange_noSettingsListeners() {
+ verify(userTracker).addCallback(capture(userTrackerCallbackCaptor), any())
+ val userCallback = userTrackerCallbackCaptor.value
+ val userId = 9
+
+ // When: User is changed
+ userCallback.onUserChanged(userId, context)
+
+ // Validate: Nothing to do, since we aren't monitoring settings
+ verify(secureSettings, never()).unregisterContentObserver(any())
+ verify(secureSettings, never()).registerContentObserverForUser(
+ any(Uri::class.java), anyBoolean(), any(), anyInt())
+ }
+ @Test
+ fun updateContentObserverRegistration_onUserChange_withSettingsListeners() {
+ // When: someone is listening to a setting
+ controller.addCallback(settingUri1,
+ Mockito.mock(Listener::class.java))
+
+ verify(userTracker).addCallback(capture(userTrackerCallbackCaptor), any())
+ val userCallback = userTrackerCallbackCaptor.value
+ val userId = 9
+
+ // Then: User is changed
+ userCallback.onUserChanged(userId, context)
+
+ // Validate: The tracker is unregistered and re-registered with the new user
+ verify(secureSettings).unregisterContentObserver(any())
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri1), eq(false), any(), eq(userId))
+ }
+
+ @Test
+ fun addCallback_onlyFirstForUriRegistersObserver() {
+ controller.addCallback(settingUri1,
+ Mockito.mock(Listener::class.java))
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri1), eq(false), any(), eq(ActivityManager.getCurrentUser()))
+
+ controller.addCallback(settingUri1,
+ Mockito.mock(Listener::class.java))
+ verify(secureSettings).registerContentObserverForUser(
+ any(Uri::class.java), anyBoolean(), any(), anyInt())
+ }
+
+ @Test
+ fun addCallback_secondUriRegistersObserver() {
+ controller.addCallback(settingUri1,
+ Mockito.mock(Listener::class.java))
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri1), eq(false), any(), eq(ActivityManager.getCurrentUser()))
+
+ controller.addCallback(settingUri2,
+ Mockito.mock(Listener::class.java))
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri2), eq(false), any(), eq(ActivityManager.getCurrentUser()))
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri1), anyBoolean(), any(), anyInt())
+ }
+
+ @Test
+ fun removeCallback_lastUnregistersObserver() {
+ val listenerSetting1 : Listener = mock()
+ val listenerSetting2 : Listener = mock()
+ controller.addCallback(settingUri1, listenerSetting1)
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri1), eq(false), any(), eq(ActivityManager.getCurrentUser()))
+
+ controller.addCallback(settingUri2, listenerSetting2)
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri2), anyBoolean(), any(), anyInt())
+
+ controller.removeCallback(settingUri2, listenerSetting2)
+ verify(secureSettings, never()).unregisterContentObserver(any())
+
+ controller.removeCallback(settingUri1, listenerSetting1)
+ verify(secureSettings).unregisterContentObserver(any())
+ }
+
+ @Test
+ fun addCallback_updatesCurrentValue() {
+ whenever(secureSettings.getStringForUser(
+ setting1, ActivityManager.getCurrentUser())).thenReturn("9")
+ whenever(secureSettings.getStringForUser(
+ setting2, ActivityManager.getCurrentUser())).thenReturn("5")
+
+ val listenerSetting1a : Listener = mock()
+ val listenerSetting1b : Listener = mock()
+ val listenerSetting2 : Listener = mock()
+
+ controller.addCallback(settingUri1, listenerSetting1a)
+ controller.addCallback(settingUri1, listenerSetting1b)
+ controller.addCallback(settingUri2, listenerSetting2)
+
+ testableLooper.processAllMessages()
+
+ verify(listenerSetting1a).onSettingChanged(
+ settingUri1, ActivityManager.getCurrentUser(), "9")
+ verify(listenerSetting1b).onSettingChanged(
+ settingUri1, ActivityManager.getCurrentUser(), "9")
+ verify(listenerSetting2).onSettingChanged(
+ settingUri2, ActivityManager.getCurrentUser(), "5")
+ }
+
+ @Test
+ fun removeCallback_noMoreUpdates() {
+ whenever(secureSettings.getStringForUser(
+ setting1, ActivityManager.getCurrentUser())).thenReturn("9")
+
+ val listenerSetting1a : Listener = mock()
+ val listenerSetting1b : Listener = mock()
+
+ // First, register
+ controller.addCallback(settingUri1, listenerSetting1a)
+ controller.addCallback(settingUri1, listenerSetting1b)
+ testableLooper.processAllMessages()
+
+ verify(secureSettings).registerContentObserverForUser(
+ any(Uri::class.java), anyBoolean(), capture(settingsObserverCaptor), anyInt())
+ verify(listenerSetting1a).onSettingChanged(
+ settingUri1, ActivityManager.getCurrentUser(), "9")
+ verify(listenerSetting1b).onSettingChanged(
+ settingUri1, ActivityManager.getCurrentUser(), "9")
+ Mockito.clearInvocations(listenerSetting1b)
+ Mockito.clearInvocations(listenerSetting1a)
+
+ // Remove one of them
+ controller.removeCallback(settingUri1, listenerSetting1a)
+
+ // On update, only remaining listener should get the callback
+ settingsObserverCaptor.value.onChange(false, settingUri1)
+ testableLooper.processAllMessages()
+
+ verify(listenerSetting1a, never()).onSettingChanged(
+ settingUri1, ActivityManager.getCurrentUser(), "9")
+ verify(listenerSetting1b).onSettingChanged(
+ settingUri1, ActivityManager.getCurrentUser(), "9")
+ }
+
+} \ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index 4f8de3eacf7a..045a63cd44a0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.phone;
+import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
import static com.google.common.truth.Truth.assertThat;
@@ -39,6 +40,8 @@ import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper;
import android.testing.TestableResources;
+import android.view.HapticFeedbackConstants;
+import android.view.ViewRootImpl;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.util.LatencyTracker;
@@ -47,6 +50,7 @@ import com.android.keyguard.logging.BiometricUnlockLogger;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
@@ -118,8 +122,11 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
private VibratorHelper mVibratorHelper;
@Mock
private BiometricUnlockLogger mLogger;
+ @Mock
+ private ViewRootImpl mViewRootImpl;
private final FakeSystemClock mSystemClock = new FakeSystemClock();
private BiometricUnlockController mBiometricUnlockController;
+ private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
@Before
public void setUp() {
@@ -142,11 +149,14 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
mNotificationMediaManager, mWakefulnessLifecycle, mScreenLifecycle,
mAuthController, mStatusBarStateController,
mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper,
- mSystemClock
+ mSystemClock,
+ mFeatureFlags
);
mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
mBiometricUnlockController.addListener(mBiometricUnlockEventsListener);
when(mUpdateMonitor.getStrongAuthTracker()).thenReturn(mStrongAuthTracker);
+ when(mStatusBarKeyguardViewManager.getViewRootImpl()).thenReturn(mViewRootImpl);
+ mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
}
@Test
@@ -484,6 +494,31 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
}
@Test
+ public void onSideFingerprintSuccess_oldPowerButtonPress_playOneWayHaptic() {
+ // GIVEN oneway haptics is enabled
+ mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
+ // GIVEN side fingerprint enrolled, last wake reason was power button
+ when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+ when(mWakefulnessLifecycle.getLastWakeReason())
+ .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
+
+ // GIVEN last wake time was 500ms ago
+ when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis());
+ mSystemClock.advanceTime(500);
+
+ // WHEN biometric fingerprint succeeds
+ givenFingerprintModeUnlockCollapsing();
+ mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT,
+ true);
+
+ // THEN vibrate the device
+ verify(mVibratorHelper).performHapticFeedback(
+ any(),
+ eq(HapticFeedbackConstants.CONFIRM)
+ );
+ }
+
+ @Test
public void onSideFingerprintSuccess_recentGestureWakeUp_playHaptic() {
// GIVEN side fingerprint enrolled, wakeup just happened
when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
@@ -503,6 +538,30 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
}
@Test
+ public void onSideFingerprintSuccess_recentGestureWakeUp_playOnewayHaptic() {
+ //GIVEN oneway haptics is enabled
+ mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
+ // GIVEN side fingerprint enrolled, wakeup just happened
+ when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+ when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis());
+
+ // GIVEN last wake reason was from a gesture
+ when(mWakefulnessLifecycle.getLastWakeReason())
+ .thenReturn(PowerManager.WAKE_REASON_GESTURE);
+
+ // WHEN biometric fingerprint succeeds
+ givenFingerprintModeUnlockCollapsing();
+ mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT,
+ true);
+
+ // THEN vibrate the device
+ verify(mVibratorHelper).performHapticFeedback(
+ any(),
+ eq(HapticFeedbackConstants.CONFIRM)
+ );
+ }
+
+ @Test
public void onSideFingerprintFail_alwaysPlaysHaptic() {
// GIVEN side fingerprint enrolled, last wake reason was recent power button
when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
@@ -518,6 +577,26 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
}
@Test
+ public void onSideFingerprintFail_alwaysPlaysOneWayHaptic() {
+ // GIVEN oneway haptics is enabled
+ mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
+ // GIVEN side fingerprint enrolled, last wake reason was recent power button
+ when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+ when(mWakefulnessLifecycle.getLastWakeReason())
+ .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
+ when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis());
+
+ // WHEN biometric fingerprint fails
+ mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
+
+ // THEN always vibrate the device
+ verify(mVibratorHelper).performHapticFeedback(
+ any(),
+ eq(HapticFeedbackConstants.REJECT)
+ );
+ }
+
+ @Test
public void onFingerprintDetect_showBouncer() {
// WHEN fingerprint detect occurs
mBiometricUnlockController.onBiometricDetected(UserHandle.USER_CURRENT,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
index 28193db01013..7de0075c45ff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
@@ -27,6 +27,7 @@ import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.shade.ShadeControllerImpl
import com.android.systemui.shade.ShadeLogger
import com.android.systemui.shade.ShadeViewController
@@ -50,6 +51,7 @@ import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import java.util.Optional
+import javax.inject.Provider
@SmallTest
class PhoneStatusBarViewControllerTest : SysuiTestCase() {
@@ -73,6 +75,8 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
@Mock
private lateinit var shadeControllerImpl: ShadeControllerImpl
@Mock
+ private lateinit var sceneInteractor: Provider<SceneInteractor>
+ @Mock
private lateinit var shadeLogger: ShadeLogger
@Mock
private lateinit var viewUtil: ViewUtil
@@ -197,6 +201,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
centralSurfacesImpl,
shadeControllerImpl,
shadeViewController,
+ sceneInteractor,
shadeLogger,
viewUtil,
configurationController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt
index 5bc98e0d19af..dbaa29bb3688 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt
@@ -18,7 +18,9 @@ package com.android.systemui.statusbar.pipeline.wifi.data.repository
import android.net.ConnectivityManager
import android.net.wifi.WifiManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.demomode.DemoMode
import com.android.systemui.demomode.DemoModeController
@@ -43,6 +45,7 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
+import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
@@ -50,6 +53,8 @@ import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@SmallTest
+@RoboPilotTest
+@RunWith(AndroidJUnit4::class)
class WifiRepositorySwitcherTest : SysuiTestCase() {
private lateinit var underTest: WifiRepositorySwitcher
private lateinit var realImpl: WifiRepositoryImpl
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
index 9cf08c03b5d1..206ac1d37074 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
@@ -16,15 +16,20 @@
package com.android.systemui.statusbar.pipeline.wifi.data.repository.prod
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
+import org.junit.runner.RunWith
@SmallTest
+@RoboPilotTest
+@RunWith(AndroidJUnit4::class)
class DisabledWifiRepositoryTest : SysuiTestCase() {
private lateinit var underTest: DisabledWifiRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
index 7007345c175c..3cf5f5249f1a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
@@ -32,7 +32,9 @@ import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.TrafficStateCallback
import android.net.wifi.WifiManager.UNKNOWN_SSID
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlots
@@ -57,6 +59,7 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
+import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
@@ -65,6 +68,8 @@ import org.mockito.MockitoAnnotations
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
+@RoboPilotTest
+@RunWith(AndroidJUnit4::class)
class WifiRepositoryImplTest : SysuiTestCase() {
private lateinit var underTest: WifiRepositoryImpl
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 820e2a031a0e..2f228a8da0c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -158,6 +158,7 @@ import com.android.wm.shell.transition.Transitions;
import org.junit.After;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -173,6 +174,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Optional;
+@Ignore("b/292153259")
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt
new file mode 100644
index 000000000000..2ef1be70000f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 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.biometrics.data.repository
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeFacePropertyRepository : FacePropertyRepository {
+ private val faceSensorInfo = MutableStateFlow<FaceSensorInfo?>(null)
+ override val sensorInfo: Flow<FaceSensorInfo?>
+ get() = faceSensorInfo
+
+ fun setSensorInfo(value: FaceSensorInfo?) {
+ faceSensorInfo.value = value
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
index 4aaf3478a31d..8c98aea6a990 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
@@ -59,7 +59,6 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository {
private val _authFlags = MutableStateFlow(AuthenticationFlags(0, 0))
override val authenticationFlags: Flow<AuthenticationFlags>
get() = _authFlags
-
fun setFingerprintEnrolled(isFingerprintEnrolled: Boolean) {
_isFingerprintEnrolled.value = isFingerprintEnrolled
}
@@ -110,4 +109,8 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository {
fun setIsNonStrongBiometricAllowed(value: Boolean) {
_isNonStrongBiometricAllowed.value = value
}
+
+ fun setIsStrongBiometricAllowed(value: Boolean) {
+ _isStrongBiometricAllowed.value = value
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
index 931798130499..f39982f54441 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
@@ -38,6 +38,8 @@ import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.keyguard.shared.model.WakefulnessState
import com.android.systemui.scene.data.repository.SceneContainerRepository
import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.RemoteUserInput
+import com.android.systemui.scene.shared.model.RemoteUserInputAction
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.scene.shared.model.SceneKey
@@ -217,5 +219,14 @@ class SceneTestUtils(
companion object {
const val CONTAINER_1 = SceneContainerNames.SYSTEM_UI_DEFAULT
const val CONTAINER_2 = "container2"
+
+ val REMOTE_INPUT_DOWN_GESTURE =
+ listOf(
+ RemoteUserInput(10f, 10f, RemoteUserInputAction.DOWN),
+ RemoteUserInput(10f, 20f, RemoteUserInputAction.MOVE),
+ RemoteUserInput(10f, 30f, RemoteUserInputAction.MOVE),
+ RemoteUserInput(10f, 40f, RemoteUserInputAction.MOVE),
+ RemoteUserInput(10f, 40f, RemoteUserInputAction.UP),
+ )
}
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index 54d1faa39be0..3d0ea9d8bef6 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -260,14 +260,6 @@ class FingerprintAuthenticationClient
final AidlSession session = getFreshDaemon();
final OperationContextExt opContext = getOperationContext();
- final ICancellationSignal cancel;
- if (session.hasContextMethods()) {
- cancel = session.getSession().authenticateWithContext(
- mOperationId, opContext.toAidlContext(getOptions()));
- } else {
- cancel = session.getSession().authenticate(mOperationId);
- }
-
getBiometricContext().subscribe(opContext, ctx -> {
if (session.hasContextMethods()) {
try {
@@ -289,7 +281,12 @@ class FingerprintAuthenticationClient
mALSProbeCallback.getProbe().enable();
}
- return cancel;
+ if (session.hasContextMethods()) {
+ return session.getSession().authenticateWithContext(
+ mOperationId, opContext.toAidlContext(getOptions()));
+ } else {
+ return session.getSession().authenticate(mOperationId);
+ }
}
@Override
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 3b779ecf77e5..626502ef07b4 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -3151,11 +3151,6 @@ public final class DisplayManagerService extends SystemService {
// with the corresponding displaydevice.
HighBrightnessModeMetadata hbmMetadata =
mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display);
- if (hbmMetadata == null) {
- Slog.wtf(TAG, "High Brightness Mode Metadata is null in DisplayManagerService for "
- + "display: " + display.getDisplayIdLocked());
- return null;
- }
if (mConfigParameterProvider.isNewPowerControllerFeatureEnabled()) {
displayPowerController = new DisplayPowerController2(
mContext, /* injector= */ null, mDisplayPowerCallbacks, mPowerHandler,
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 46b543b9cfb9..4880ea96d5b0 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -450,6 +450,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
private float[] mNitsRange;
private final BrightnessRangeController mBrightnessRangeController;
+
+ @Nullable
private final HighBrightnessModeMetadata mHighBrightnessModeMetadata;
private final BrightnessThrottler mBrightnessThrottler;
@@ -722,7 +724,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
setUpAutoBrightness(resources, handler);
- mColorFadeEnabled = !ActivityManager.isLowRamDeviceStatic();
+ mColorFadeEnabled = !ActivityManager.isLowRamDeviceStatic()
+ && !resources.getBoolean(
+ com.android.internal.R.bool.config_displayColorFadeDisabled);
mColorFadeFadesConfig = resources.getBoolean(
com.android.internal.R.bool.config_animateScreenLights);
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 1d8b4949bd86..ad2b471cd7ed 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -611,7 +611,9 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal
setUpAutoBrightness(resources, handler);
- mColorFadeEnabled = mInjector.isColorFadeEnabled();
+ mColorFadeEnabled = mInjector.isColorFadeEnabled()
+ && !resources.getBoolean(
+ com.android.internal.R.bool.config_displayColorFadeDisabled);
mColorFadeFadesConfig = resources.getBoolean(
R.bool.config_animateScreenLights);
diff --git a/services/core/java/com/android/server/display/HighBrightnessModeController.java b/services/core/java/com/android/server/display/HighBrightnessModeController.java
index 11160a532609..c04c2793b3c5 100644
--- a/services/core/java/com/android/server/display/HighBrightnessModeController.java
+++ b/services/core/java/com/android/server/display/HighBrightnessModeController.java
@@ -16,6 +16,7 @@
package com.android.server.display;
+import android.annotation.Nullable;
import android.content.Context;
import android.database.ContentObserver;
import android.hardware.display.BrightnessInfo;
@@ -75,6 +76,8 @@ class HighBrightnessModeController {
private final Injector mInjector;
private HdrListener mHdrListener;
+
+ @Nullable
private HighBrightnessModeData mHbmData;
private HdrBrightnessDeviceConfig mHdrBrightnessCfg;
private IBinder mRegisteredDisplayToken;
@@ -107,7 +110,9 @@ class HighBrightnessModeController {
* If HBM is currently running, this is the start time and set of all events,
* for the current HBM session.
*/
- private HighBrightnessModeMetadata mHighBrightnessModeMetadata = null;
+ @Nullable
+ private HighBrightnessModeMetadata mHighBrightnessModeMetadata;
+
HighBrightnessModeController(Handler handler, int width, int height, IBinder displayToken,
String displayUniqueId, float brightnessMin, float brightnessMax,
HighBrightnessModeData hbmData, HdrBrightnessDeviceConfig hdrBrightnessCfg,
@@ -310,23 +315,29 @@ class HighBrightnessModeController {
pw.println(" mBrightnessMax=" + mBrightnessMax);
pw.println(" remainingTime=" + calculateRemainingTime(mClock.uptimeMillis()));
pw.println(" mIsTimeAvailable= " + mIsTimeAvailable);
- pw.println(" mRunningStartTimeMillis="
- + TimeUtils.formatUptime(mHighBrightnessModeMetadata.getRunningStartTimeMillis()));
pw.println(" mIsBlockedByLowPowerMode=" + mIsBlockedByLowPowerMode);
pw.println(" width*height=" + mWidth + "*" + mHeight);
- pw.println(" mEvents=");
- final long currentTime = mClock.uptimeMillis();
- long lastStartTime = currentTime;
- long runningStartTimeMillis = mHighBrightnessModeMetadata.getRunningStartTimeMillis();
- if (runningStartTimeMillis != -1) {
- lastStartTime = dumpHbmEvent(pw, new HbmEvent(runningStartTimeMillis, currentTime));
- }
- for (HbmEvent event : mHighBrightnessModeMetadata.getHbmEventQueue()) {
- if (lastStartTime > event.getEndTimeMillis()) {
- pw.println(" event: [normal brightness]: "
- + TimeUtils.formatDuration(lastStartTime - event.getEndTimeMillis()));
+
+ if (mHighBrightnessModeMetadata != null) {
+ pw.println(" mRunningStartTimeMillis="
+ + TimeUtils.formatUptime(
+ mHighBrightnessModeMetadata.getRunningStartTimeMillis()));
+ pw.println(" mEvents=");
+ final long currentTime = mClock.uptimeMillis();
+ long lastStartTime = currentTime;
+ long runningStartTimeMillis = mHighBrightnessModeMetadata.getRunningStartTimeMillis();
+ if (runningStartTimeMillis != -1) {
+ lastStartTime = dumpHbmEvent(pw, new HbmEvent(runningStartTimeMillis, currentTime));
}
- lastStartTime = dumpHbmEvent(pw, event);
+ for (HbmEvent event : mHighBrightnessModeMetadata.getHbmEventQueue()) {
+ if (lastStartTime > event.getEndTimeMillis()) {
+ pw.println(" event: [normal brightness]: "
+ + TimeUtils.formatDuration(lastStartTime - event.getEndTimeMillis()));
+ }
+ lastStartTime = dumpHbmEvent(pw, event);
+ }
+ } else {
+ pw.println(" mHighBrightnessModeMetadata=null");
}
}
@@ -353,7 +364,7 @@ class HighBrightnessModeController {
}
private boolean deviceSupportsHbm() {
- return mHbmData != null;
+ return mHbmData != null && mHighBrightnessModeMetadata != null;
}
private long calculateRemainingTime(long currentTime) {
diff --git a/services/core/java/com/android/server/display/HighBrightnessModeMetadataMapper.java b/services/core/java/com/android/server/display/HighBrightnessModeMetadataMapper.java
index 76702d3f6f8c..9e6f0eb93831 100644
--- a/services/core/java/com/android/server/display/HighBrightnessModeMetadataMapper.java
+++ b/services/core/java/com/android/server/display/HighBrightnessModeMetadataMapper.java
@@ -41,6 +41,9 @@ class HighBrightnessModeMetadataMapper {
+ display.getDisplayIdLocked());
return null;
}
+ if (device.getDisplayDeviceConfig().getHighBrightnessModeData() == null) {
+ return null;
+ }
final String uniqueId = device.getUniqueId();
diff --git a/services/core/java/com/android/server/display/TEST_MAPPING b/services/core/java/com/android/server/display/TEST_MAPPING
index c4a566fd7b62..5e4e27069fb1 100644
--- a/services/core/java/com/android/server/display/TEST_MAPPING
+++ b/services/core/java/com/android/server/display/TEST_MAPPING
@@ -1,15 +1,7 @@
{
"presubmit": [
{
- "name": "FrameworksMockingServicesTests",
- "options": [
- {"include-filter": "com.android.server.display"},
- {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
- {"exclude-annotation": "androidx.test.filters.FlakyTest"}
- ]
- },
- {
- "name": "FrameworksServicesTests",
+ "name": "DisplayServiceTests",
"options": [
{"include-filter": "com.android.server.display"},
{"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
@@ -31,5 +23,14 @@
}
]
}
+ ],
+ "postsubmit": [
+ {
+ "name": "DisplayServiceTests",
+ "options": [
+ {"include-filter": "com.android.server.display"},
+ {"exclude-annotation": "org.junit.Ignore"}
+ ]
+ }
]
-} \ No newline at end of file
+}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 3a8f9d5c35e6..5c80291f7f46 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -116,6 +116,7 @@ import com.android.server.DisplayThread;
import com.android.server.LocalServices;
import com.android.server.Watchdog;
import com.android.server.input.InputManagerInternal.LidSwitchCallback;
+import com.android.server.inputmethod.InputMethodManagerInternal;
import com.android.server.policy.WindowManagerPolicy;
import libcore.io.IoUtils;
@@ -165,6 +166,8 @@ public class InputManagerService extends IInputManager.Stub
private final InputManagerHandler mHandler;
private DisplayManagerInternal mDisplayManagerInternal;
+ private InputMethodManagerInternal mInputMethodManagerInternal;
+
// Context cache used for loading pointer resources.
private Context mPointerIconDisplayContext;
@@ -510,6 +513,8 @@ public class InputManagerService extends IInputManager.Stub
}
mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
+ mInputMethodManagerInternal =
+ LocalServices.getService(InputMethodManagerInternal.class);
mSettingsObserver.registerAndUpdate();
@@ -2790,6 +2795,13 @@ public class InputManagerService extends IInputManager.Stub
yPosition)).sendToTarget();
}
+ // Native callback.
+ @SuppressWarnings("unused")
+ boolean isInputMethodConnectionActive() {
+ return mInputMethodManagerInternal != null
+ && mInputMethodManagerInternal.isAnyInputConnectionActive();
+ }
+
/**
* Callback interface implemented by the Window Manager.
*/
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
index 8c7658e53dcd..08503cb2e9f8 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
@@ -186,6 +186,12 @@ public abstract class InputMethodManagerInternal {
public abstract void switchKeyboardLayout(int direction);
/**
+ * Returns true if any InputConnection is currently active.
+ * {@hide}
+ */
+ public abstract boolean isAnyInputConnectionActive();
+
+ /**
* Fake implementation of {@link InputMethodManagerInternal}. All the methods do nothing.
*/
private static final InputMethodManagerInternal NOP =
@@ -268,6 +274,11 @@ public abstract class InputMethodManagerInternal {
@Override
public void switchKeyboardLayout(int direction) {
}
+
+ @Override
+ public boolean isAnyInputConnectionActive() {
+ return false;
+ }
};
/**
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index c5fbcb968ab6..cfcb4620bf25 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -5937,6 +5937,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
}
}
}
+
+ /**
+ * Returns true if any InputConnection is currently active.
+ */
+ @Override
+ public boolean isAnyInputConnectionActive() {
+ return mCurInputConnection != null;
+ }
}
@BinderThread
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 398e470d9fda..61d0afeb7717 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -117,6 +117,10 @@ public final class MediaProjectionManagerService extends SystemService
// WindowManagerService -> MediaProjectionManagerService -> DisplayManagerService
// See mediaprojection.md
private final Object mLock = new Object();
+ // A handler for posting tasks that must interact with a service holding another lock,
+ // especially for services that will eventually acquire the WindowManager lock.
+ @NonNull private final Handler mHandler;
+
private final Map<IBinder, IBinder.DeathRecipient> mDeathEaters;
private final CallbackDelegate mCallbackDelegate;
@@ -145,6 +149,8 @@ public final class MediaProjectionManagerService extends SystemService
super(context);
mContext = context;
mInjector = injector;
+ // Post messages on the main thread; no need for a separate thread.
+ mHandler = new Handler(Looper.getMainLooper());
mClock = injector.createClock();
mDeathEaters = new ArrayMap<IBinder, IBinder.DeathRecipient>();
mCallbackDelegate = new CallbackDelegate(injector.createCallbackLooper());
@@ -243,14 +249,17 @@ public final class MediaProjectionManagerService extends SystemService
if (!mProjectionGrant.requiresForegroundService()) {
return;
}
+ }
- if (mActivityManagerInternal.hasRunningForegroundService(
- uid, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)) {
- // If there is any process within this UID running a FGS
- // with the mediaProjection type, that's Okay.
- return;
- }
+ // Run outside the lock when calling into ActivityManagerService.
+ if (mActivityManagerInternal.hasRunningForegroundService(
+ uid, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)) {
+ // If there is any process within this UID running a FGS
+ // with the mediaProjection type, that's Okay.
+ return;
+ }
+ synchronized (mLock) {
mProjectionGrant.stop();
}
}
@@ -867,7 +876,6 @@ public final class MediaProjectionManagerService extends SystemService
mTargetSdkVersion = targetSdkVersion;
mIsPrivileged = isPrivileged;
mCreateTimeMs = mClock.uptimeMillis();
- // TODO(b/267740338): Add unit test.
mActivityManagerInternal.notifyMediaProjectionEvent(uid, asBinder(),
MEDIA_PROJECTION_TOKEN_EVENT_CREATED);
}
@@ -924,6 +932,10 @@ public final class MediaProjectionManagerService extends SystemService
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
+ // Cache result of calling into ActivityManagerService outside of the lock, to prevent
+ // deadlock with WindowManagerService.
+ final boolean hasFGS = mActivityManagerInternal.hasRunningForegroundService(
+ uid, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
synchronized (mLock) {
if (isCurrentProjection(asBinder())) {
Slog.w(TAG, "UID " + Binder.getCallingUid()
@@ -935,9 +947,7 @@ public final class MediaProjectionManagerService extends SystemService
}
if (REQUIRE_FG_SERVICE_FOR_PROJECTION
- && requiresForegroundService()
- && !mActivityManagerInternal.hasRunningForegroundService(
- uid, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)) {
+ && requiresForegroundService() && !hasFGS) {
throw new SecurityException("Media projections require a foreground service"
+ " of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION");
}
@@ -1026,10 +1036,11 @@ public final class MediaProjectionManagerService extends SystemService
mToken = null;
unregisterCallback(mCallback);
mCallback = null;
- // TODO(b/267740338): Add unit test.
- mActivityManagerInternal.notifyMediaProjectionEvent(uid, asBinder(),
- MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED);
}
+ // Run on a separate thread, to ensure no lock is held when calling into
+ // ActivityManagerService.
+ mHandler.post(() -> mActivityManagerInternal.notifyMediaProjectionEvent(uid, asBinder(),
+ MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED));
}
@Override // Binder call
diff --git a/services/core/java/com/android/server/media/projection/mediaprojection.md b/services/core/java/com/android/server/media/projection/mediaprojection.md
index bccdf3411903..34e7ecc6c6c5 100644
--- a/services/core/java/com/android/server/media/projection/mediaprojection.md
+++ b/services/core/java/com/android/server/media/projection/mediaprojection.md
@@ -11,6 +11,11 @@ Calls must follow the below invocation order while holding locks:
`WindowManagerService -> MediaProjectionManagerService -> DisplayManagerService`
+`MediaProjectionManagerService` should never lock when calling into a service that may acquire
+the `WindowManagerService` global lock (for example,
+`MediaProjectionManagerService -> ActivityManagerService` may result in deadlock, since
+`ActivityManagerService -> WindowManagerService`).
+
### Justification
`MediaProjectionManagerService` calls into `WindowManagerService` in the below cases. While handling
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 6f0b0bcf0112..c2b21beb10df 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -2491,6 +2491,16 @@ public class NotificationManagerService extends SystemService {
getContext().registerReceiver(mReviewNotificationPermissionsReceiver,
ReviewNotificationPermissionsReceiver.getFilter(),
Context.RECEIVER_NOT_EXPORTED);
+
+ mAppOps.startWatchingMode(AppOpsManager.OP_POST_NOTIFICATION, null,
+ new AppOpsManager.OnOpChangedInternalListener() {
+ @Override
+ public void onOpChanged(@NonNull String op, @NonNull String packageName,
+ int userId) {
+ mHandler.post(
+ () -> handleNotificationPermissionChange(packageName, userId));
+ }
+ });
}
/**
@@ -3281,6 +3291,11 @@ public class NotificationManagerService extends SystemService {
return new MultiRateLimiter.Builder(getContext()).addRateLimits(TOAST_RATE_LIMITS).build();
}
+ protected int checkComponentPermission(String permission, int uid, int owningUid,
+ boolean exported) {
+ return ActivityManager.checkComponentPermission(permission, uid, owningUid, exported);
+ }
+
@VisibleForTesting
final IBinder mService = new INotificationManager.Stub() {
// Toasts
@@ -3557,13 +3572,9 @@ public class NotificationManagerService extends SystemService {
.setPackageName(pkg)
.setSubtype(enabled ? 1 : 0));
mNotificationChannelLogger.logAppNotificationsAllowed(uid, pkg, enabled);
- // Now, cancel any outstanding notifications that are part of a just-disabled app
- if (!enabled) {
- cancelAllNotificationsInt(MY_UID, MY_PID, pkg, null, 0, 0,
- UserHandle.getUserId(uid), REASON_PACKAGE_BANNED);
- }
- handleSavePolicyFile();
+ // Outstanding notifications from this package will be cancelled as soon as we get the
+ // callback from AppOpsManager.
}
/**
@@ -5238,10 +5249,11 @@ public class NotificationManagerService extends SystemService {
}
private boolean checkPolicyAccess(String pkg) {
+ final int uid;
try {
- int uid = getContext().getPackageManager().getPackageUidAsUser(pkg,
+ uid = getContext().getPackageManager().getPackageUidAsUser(pkg,
UserHandle.getCallingUserId());
- if (PackageManager.PERMISSION_GRANTED == ActivityManager.checkComponentPermission(
+ if (PackageManager.PERMISSION_GRANTED == checkComponentPermission(
android.Manifest.permission.MANAGE_NOTIFICATIONS, uid,
-1, true)) {
return true;
@@ -5252,8 +5264,8 @@ public class NotificationManagerService extends SystemService {
//TODO(b/169395065) Figure out if this flow makes sense in Device Owner mode.
return checkPackagePolicyAccess(pkg)
|| mListeners.isComponentEnabledForPackage(pkg)
- || (mDpm != null && (mDpm.isActiveProfileOwner(Binder.getCallingUid())
- || mDpm.isActiveDeviceOwner(Binder.getCallingUid())));
+ || (mDpm != null && (mDpm.isActiveProfileOwner(uid)
+ || mDpm.isActiveDeviceOwner(uid)));
}
@Override
@@ -5886,6 +5898,23 @@ public class NotificationManagerService extends SystemService {
}
};
+ private void handleNotificationPermissionChange(String pkg, @UserIdInt int userId) {
+ if (!mUmInternal.isUserInitialized(userId)) {
+ return; // App-op "updates" are sent when starting a new user the first time.
+ }
+ int uid = mPackageManagerInternal.getPackageUid(pkg, 0, userId);
+ if (uid == INVALID_UID) {
+ Log.e(TAG, String.format("No uid found for %s, %s!", pkg, userId));
+ return;
+ }
+ boolean hasPermission = mPermissionHelper.hasPermission(uid);
+ if (!hasPermission) {
+ cancelAllNotificationsInt(MY_UID, MY_PID, pkg, /* channelId= */ null,
+ /* mustHaveFlags= */ 0, /* mustNotHaveFlags= */ 0, userId,
+ REASON_PACKAGE_BANNED);
+ }
+ }
+
protected void checkNotificationListenerAccess() {
if (!isCallerSystemOrPhone()) {
getContext().enforceCallingPermission(
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index efd8b6d9a943..a5123311d499 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -140,6 +140,20 @@ public interface StatusBarManagerInternal {
boolean showShutdownUi(boolean isReboot, String requestString);
/**
+ * Notify system UI the immersive prompt should be dismissed as confirmed, and the confirmed
+ * status should be saved without user clicking on the button. This could happen when a user
+ * swipe on the edge with the confirmation prompt showing.
+ */
+ void confirmImmersivePrompt();
+
+ /**
+ * Notify System UI that the system get into or exit immersive mode.
+ * @param rootDisplayAreaId The changed display area Id.
+ * @param isImmersiveMode {@code true} if the display area get into immersive mode.
+ */
+ void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode);
+
+ /**
* Show a rotation suggestion that a user may approve to rotate the screen.
*
* @param rotation rotation suggestion
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index cc849b6fbf91..40e9c1305f01 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -27,6 +27,7 @@ import static android.app.StatusBarManager.NavBarMode;
import static android.app.StatusBarManager.SessionFlags;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.ViewRootImpl.CLIENT_TRANSIENT;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY;
import android.Manifest;
@@ -638,6 +639,31 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D
return false;
}
+ @Override
+ public void confirmImmersivePrompt() {
+ if (mBar == null) {
+ return;
+ }
+ try {
+ mBar.confirmImmersivePrompt();
+ } catch (RemoteException ex) {
+ }
+ }
+
+ @Override
+ public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {
+ if (mBar == null) {
+ return;
+ }
+ if (!CLIENT_TRANSIENT) {
+ // Only call from here when the client transient is not enabled.
+ try {
+ mBar.immersiveModeChanged(rootDisplayAreaId, isImmersiveMode);
+ } catch (RemoteException ex) {
+ }
+ }
+ }
+
// TODO(b/118592525): support it per display if necessary.
@Override
public void onProposedRotationChanged(int rotation, boolean isValid) {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index ee7dc5007d97..309a9c0e0372 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -2895,6 +2895,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
checkPermission(android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT);
final long ident = Binder.clearCallingIdentity();
try {
+ List<WallpaperData> pendingColorExtraction = new ArrayList<>();
synchronized (mLock) {
WallpaperData wallpaper = mWallpaperMap.get(mCurrentUserId);
WallpaperData lockWallpaper = mLockWallpaperMap.get(mCurrentUserId);
@@ -2930,7 +2931,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
// Need to extract colors again to re-calculate dark hints after
// applying dimming.
wp.mIsColorExtractedFromDim = true;
- notifyWallpaperColorsChanged(wp, wp.mWhich);
+ pendingColorExtraction.add(wp);
changed = true;
}
}
@@ -2962,6 +2963,9 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
}
}
}
+ for (WallpaperData wp: pendingColorExtraction) {
+ notifyWallpaperColorsChanged(wp, wp.mWhich);
+ }
} finally {
Binder.restoreCallingIdentity(ident);
}
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index d187d2321e21..3c650e32beb9 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -2032,6 +2032,13 @@ class ActivityStarter {
}
// ASM rules have failed. Log why
+ return logAsmFailureAndCheckFeatureEnabled(r, newTask, targetTask, shouldBlockActivityStart,
+ taskToFront);
+ }
+
+ private boolean logAsmFailureAndCheckFeatureEnabled(ActivityRecord r, boolean newTask,
+ Task targetTask, boolean shouldBlockActivityStart, boolean taskToFront) {
+ // ASM rules have failed. Log why
ActivityRecord targetTopActivity = targetTask == null ? null
: targetTask.getActivity(ar -> !ar.finishing && !ar.isAlwaysOnTop());
@@ -2041,6 +2048,13 @@ class ActivityStarter {
? FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__ACTIVITY_START_SAME_TASK
: FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__ACTIVITY_START_DIFFERENT_TASK);
+ boolean blockActivityStartAndFeatureEnabled = ActivitySecurityModelFeatureFlags
+ .shouldRestrictActivitySwitch(mCallingUid)
+ && shouldBlockActivityStart;
+
+ String asmDebugInfo = getDebugInfoForActivitySecurity("Launch", r, targetTask,
+ targetTopActivity, blockActivityStartAndFeatureEnabled, /*taskToFront*/taskToFront);
+
FrameworkStatsLog.write(FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED,
/* caller_uid */
mSourceRecord != null ? mSourceRecord.getUid() : mCallingUid,
@@ -2069,24 +2083,21 @@ class ActivityStarter {
targetTask != null && mSourceRecord != null
&& !targetTask.equals(mSourceRecord.getTask()) && targetTask.isVisible(),
/* bal_code */
- mBalCode
+ mBalCode,
+ /* task_stack */
+ asmDebugInfo
);
- boolean blockActivityStartAndFeatureEnabled = ActivitySecurityModelFeatureFlags
- .shouldRestrictActivitySwitch(mCallingUid)
- && shouldBlockActivityStart;
-
String launchedFromPackageName = r.launchedFromPackage;
if (ActivitySecurityModelFeatureFlags.shouldShowToast(mCallingUid)) {
String toastText = ActivitySecurityModelFeatureFlags.DOC_LINK
+ (blockActivityStartAndFeatureEnabled ? " blocked " : " would block ")
+ getApplicationLabel(mService.mContext.getPackageManager(),
- launchedFromPackageName);
+ launchedFromPackageName);
UiThread.getHandler().post(() -> Toast.makeText(mService.mContext,
toastText, Toast.LENGTH_LONG).show());
- logDebugInfoForActivitySecurity("Launch", r, targetTask, targetTopActivity,
- blockActivityStartAndFeatureEnabled, /* taskToFront */ taskToFront);
+ Slog.i(TAG, asmDebugInfo);
}
if (blockActivityStartAndFeatureEnabled) {
@@ -2104,7 +2115,7 @@ class ActivityStarter {
}
/** Only called when an activity launch may be blocked, which should happen very rarely */
- private void logDebugInfoForActivitySecurity(String action, ActivityRecord r, Task targetTask,
+ private String getDebugInfoForActivitySecurity(String action, ActivityRecord r, Task targetTask,
ActivityRecord targetTopActivity, boolean blockActivityStartAndFeatureEnabled,
boolean taskToFront) {
final String prefix = "[ASM] ";
@@ -2165,7 +2176,7 @@ class ActivityStarter {
joiner.add(prefix + "BalCode: " + balCodeToString(mBalCode));
joiner.add(prefix + "------ Activity Security " + action + " Debug Logging End ------");
- Slog.i(TAG, joiner.toString());
+ return joiner.toString();
}
/**
@@ -2339,7 +2350,7 @@ class ActivityStarter {
+ ActivitySecurityModelFeatureFlags.DOC_LINK,
Toast.LENGTH_LONG).show());
- logDebugInfoForActivitySecurity("Clear Top", mStartActivity, targetTask, targetTaskTop,
+ getDebugInfoForActivitySecurity("Clear Top", mStartActivity, targetTask, targetTaskTop,
shouldBlockActivityStart, /* taskToFront */ true);
}
}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 5553600b403f..cc130c407690 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -1752,7 +1752,9 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks {
/* multi_window */
false,
/* bal_code */
- -1
+ -1,
+ /* task_stack */
+ null
);
boolean restrictActivitySwitch = ActivitySecurityModelFeatureFlags
diff --git a/services/core/java/com/android/server/wm/Dimmer.java b/services/core/java/com/android/server/wm/Dimmer.java
index 89f044bdd163..d7667d8ce7a8 100644
--- a/services/core/java/com/android/server/wm/Dimmer.java
+++ b/services/core/java/com/android/server/wm/Dimmer.java
@@ -215,8 +215,7 @@ class Dimmer {
return mDimState;
}
- private void dim(SurfaceControl.Transaction t, WindowContainer container, int relativeLayer,
- float alpha, int blurRadius) {
+ private void dim(WindowContainer container, int relativeLayer, float alpha, int blurRadius) {
final DimState d = getDimState(container);
if (d == null) {
@@ -226,6 +225,7 @@ class Dimmer {
// The dim method is called from WindowState.prepareSurfaces(), which is always called
// in the correct Z from lowest Z to highest. This ensures that the dim layer is always
// relative to the highest Z layer with a dim.
+ SurfaceControl.Transaction t = mHost.getPendingTransaction();
t.setRelativeLayer(d.mDimLayer, container.getSurfaceControl(), relativeLayer);
t.setAlpha(d.mDimLayer, alpha);
t.setBackgroundBlurRadius(d.mDimLayer, blurRadius);
@@ -238,26 +238,23 @@ class Dimmer {
* for each call to {@link WindowContainer#prepareSurfaces} the Dim state will be reset
* and the child should call dimAbove again to request the Dim to continue.
*
- * @param t A transaction in which to apply the Dim.
* @param container The container which to dim above. Should be a child of our host.
* @param alpha The alpha at which to Dim.
*/
- void dimAbove(SurfaceControl.Transaction t, WindowContainer container, float alpha) {
- dim(t, container, 1, alpha, 0);
+ void dimAbove(WindowContainer container, float alpha) {
+ dim(container, 1, alpha, 0);
}
/**
* Like {@link #dimAbove} but places the dim below the given container.
*
- * @param t A transaction in which to apply the Dim.
* @param container The container which to dim below. Should be a child of our host.
* @param alpha The alpha at which to Dim.
* @param blurRadius The amount of blur added to the Dim.
*/
- void dimBelow(SurfaceControl.Transaction t, WindowContainer container, float alpha,
- int blurRadius) {
- dim(t, container, -1, alpha, blurRadius);
+ void dimBelow(WindowContainer container, float alpha, int blurRadius) {
+ dim(container, -1, alpha, blurRadius);
}
/**
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 2309e5891a30..64c2c5d9c228 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1935,7 +1935,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
} else if (mFixedRotationLaunchingApp != null && r == null) {
mWmService.mDisplayNotificationController.dispatchFixedRotationFinished(this);
// Keep async rotation controller if the next transition of display is requested.
- if (!mTransitionController.isCollecting(this)) {
+ if (!mTransitionController.hasCollectingRotationChange(this, getRotation())) {
finishAsyncRotationIfPossible();
}
}
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 2717a6a8ab04..354b0db77382 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -22,6 +22,7 @@ import static android.view.InsetsFrameProvider.SOURCE_ARBITRARY_RECTANGLE;
import static android.view.InsetsFrameProvider.SOURCE_CONTAINER_BOUNDS;
import static android.view.InsetsFrameProvider.SOURCE_DISPLAY;
import static android.view.InsetsFrameProvider.SOURCE_FRAME;
+import static android.view.ViewRootImpl.CLIENT_IMMERSIVE_CONFIRMATION;
import static android.view.ViewRootImpl.CLIENT_TRANSIENT;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
@@ -38,6 +39,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACK
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
@@ -194,6 +196,8 @@ public class DisplayPolicy {
private final ScreenshotHelper mScreenshotHelper;
private final Object mServiceAcquireLock = new Object();
+ private long mPanicTime;
+ private final long mPanicThresholdMs;
private StatusBarManagerInternal mStatusBarManagerInternal;
@Px
@@ -246,6 +250,8 @@ public class DisplayPolicy {
private volatile boolean mKeyguardDrawComplete;
private volatile boolean mWindowManagerDrawComplete;
+ private boolean mImmersiveConfirmationWindowExists;
+
private WindowState mStatusBar = null;
private volatile WindowState mNotificationShade;
private WindowState mNavigationBar = null;
@@ -402,6 +408,7 @@ public class DisplayPolicy {
mCanSystemBarsBeShownByUser = !r.getBoolean(
R.bool.config_remoteInsetsControllerControlsSystemBars) || r.getBoolean(
R.bool.config_remoteInsetsControllerSystemBarsCanBeShownByUserAction);
+ mPanicThresholdMs = r.getInteger(R.integer.config_immersive_mode_confirmation_panic);
mAccessibilityManager = (AccessibilityManager) mContext.getSystemService(
Context.ACCESSIBILITY_SERVICE);
@@ -623,8 +630,12 @@ public class DisplayPolicy {
};
displayContent.mAppTransition.registerListenerLocked(mAppTransitionListener);
displayContent.mTransitionController.registerLegacyListener(mAppTransitionListener);
- mImmersiveModeConfirmation = new ImmersiveModeConfirmation(mContext, looper,
- mService.mVrModeEnabled, mCanSystemBarsBeShownByUser);
+ if (CLIENT_TRANSIENT || CLIENT_IMMERSIVE_CONFIRMATION) {
+ mImmersiveModeConfirmation = null;
+ } else {
+ mImmersiveModeConfirmation = new ImmersiveModeConfirmation(mContext, looper,
+ mService.mVrModeEnabled, mCanSystemBarsBeShownByUser);
+ }
// TODO: Make it can take screenshot on external display
mScreenshotHelper = displayContent.isDefaultDisplay
@@ -1075,6 +1086,9 @@ public class DisplayPolicy {
mNavigationBar = win;
break;
}
+ if ((attrs.privateFlags & PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW) != 0) {
+ mImmersiveConfirmationWindowExists = true;
+ }
if (attrs.providedInsets != null) {
for (int i = attrs.providedInsets.length - 1; i >= 0; i--) {
final InsetsFrameProvider provider = attrs.providedInsets[i];
@@ -1234,6 +1248,9 @@ public class DisplayPolicy {
}
}
mInsetsSourceWindowsExceptIme.remove(win);
+ if ((win.mAttrs.privateFlags & PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW) != 0) {
+ mImmersiveConfirmationWindowExists = false;
+ }
}
WindowState getStatusBar() {
@@ -2171,7 +2188,11 @@ public class DisplayPolicy {
}
}
}
- mImmersiveModeConfirmation.confirmCurrentPrompt();
+ if (CLIENT_IMMERSIVE_CONFIRMATION || CLIENT_TRANSIENT) {
+ mStatusBarManagerInternal.confirmImmersivePrompt();
+ } else {
+ mImmersiveModeConfirmation.confirmCurrentPrompt();
+ }
}
boolean isKeyguardShowing() {
@@ -2221,7 +2242,8 @@ public class DisplayPolicy {
// Immersive mode confirmation should never affect the system bar visibility, otherwise
// it will unhide the navigation bar and hide itself.
- if (winCandidate.getAttrs().token == mImmersiveModeConfirmation.getWindowToken()) {
+ if ((winCandidate.getAttrs().privateFlags
+ & PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW) != 0) {
if (mNotificationShade != null && mNotificationShade.canReceiveKeys()) {
// Let notification shade control the system bar visibility.
winCandidate = mNotificationShade;
@@ -2389,9 +2411,16 @@ public class DisplayPolicy {
// The immersive confirmation window should be attached to the immersive window root.
final RootDisplayArea root = win.getRootDisplayArea();
final int rootDisplayAreaId = root == null ? FEATURE_UNDEFINED : root.mFeatureId;
- mImmersiveModeConfirmation.immersiveModeChangedLw(rootDisplayAreaId, isImmersiveMode,
- mService.mPolicy.isUserSetupComplete(),
- isNavBarEmpty(disableFlags));
+ if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) {
+ mImmersiveModeConfirmation.immersiveModeChangedLw(rootDisplayAreaId,
+ isImmersiveMode,
+ mService.mPolicy.isUserSetupComplete(),
+ isNavBarEmpty(disableFlags));
+ } else {
+ // TODO (b/277290737): Move this to the client side, instead of using a proxy.
+ callStatusBarSafely(statusBar -> statusBar.immersiveModeChanged(rootDisplayAreaId,
+ isImmersiveMode));
+ }
}
// Show transient bars for panic if needed.
@@ -2604,16 +2633,39 @@ public class DisplayPolicy {
void onPowerKeyDown(boolean isScreenOn) {
// Detect user pressing the power button in panic when an application has
// taken over the whole screen.
- boolean panic = mImmersiveModeConfirmation.onPowerKeyDown(isScreenOn,
- SystemClock.elapsedRealtime(), isImmersiveMode(mSystemUiControllingWindow),
- isNavBarEmpty(mLastDisableFlags));
+ boolean panic = false;
+ if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) {
+ panic = mImmersiveModeConfirmation.onPowerKeyDown(isScreenOn,
+ SystemClock.elapsedRealtime(), isImmersiveMode(mSystemUiControllingWindow),
+ isNavBarEmpty(mLastDisableFlags));
+ } else {
+ panic = isPowerKeyDownPanic(isScreenOn, SystemClock.elapsedRealtime(),
+ isImmersiveMode(mSystemUiControllingWindow), isNavBarEmpty(mLastDisableFlags));
+ }
if (panic) {
mHandler.post(mHiddenNavPanic);
}
}
+ private boolean isPowerKeyDownPanic(boolean isScreenOn, long time, boolean inImmersiveMode,
+ boolean navBarEmpty) {
+ if (!isScreenOn && (time - mPanicTime < mPanicThresholdMs)) {
+ // turning the screen back on within the panic threshold
+ return !mImmersiveConfirmationWindowExists;
+ }
+ if (isScreenOn && inImmersiveMode && !navBarEmpty) {
+ // turning the screen off, remember if we were in immersive mode
+ mPanicTime = time;
+ } else {
+ mPanicTime = 0;
+ }
+ return false;
+ }
+
void onVrStateChangedLw(boolean enabled) {
- mImmersiveModeConfirmation.onVrStateChangedLw(enabled);
+ if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) {
+ mImmersiveModeConfirmation.onVrStateChangedLw(enabled);
+ }
}
/**
@@ -2626,7 +2678,9 @@ public class DisplayPolicy {
* {@link ActivityManager#LOCK_TASK_MODE_PINNED}.
*/
public void onLockTaskStateChangedLw(int lockTaskState) {
- mImmersiveModeConfirmation.onLockTaskModeChangedLw(lockTaskState);
+ if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) {
+ mImmersiveModeConfirmation.onLockTaskModeChangedLw(lockTaskState);
+ }
}
/** Called when a {@link android.os.PowerManager#USER_ACTIVITY_EVENT_TOUCH} is sent. */
@@ -2643,7 +2697,11 @@ public class DisplayPolicy {
}
boolean onSystemUiSettingsChanged() {
- return mImmersiveModeConfirmation.onSettingChanged(mService.mCurrentUserId);
+ if (CLIENT_TRANSIENT || CLIENT_IMMERSIVE_CONFIRMATION) {
+ return false;
+ } else {
+ return mImmersiveModeConfirmation.onSettingChanged(mService.mCurrentUserId);
+ }
}
/**
@@ -2857,7 +2915,9 @@ public class DisplayPolicy {
mDisplayContent.mTransitionController.unregisterLegacyListener(mAppTransitionListener);
mHandler.post(mGestureNavigationSettingsObserver::unregister);
mHandler.post(mForceShowNavBarSettingsObserver::unregister);
- mImmersiveModeConfirmation.release();
+ if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) {
+ mImmersiveModeConfirmation.release();
+ }
if (mService.mPointerLocationEnabled) {
setPointerLocationEnabled(false);
}
diff --git a/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java b/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java
index 56edde09f747..bd08dff9481b 100644
--- a/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java
+++ b/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java
@@ -19,6 +19,7 @@ package com.android.server.wm;
import static android.app.ActivityManager.LOCK_TASK_MODE_LOCKED;
import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.ViewRootImpl.CLIENT_TRANSIENT;
import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID;
@@ -229,7 +230,8 @@ public class ImmersiveModeConfirmation {
lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~Type.statusBars());
// Trusted overlay so touches outside the touchable area are allowed to pass through
lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
- | WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
+ | WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+ | WindowManager.LayoutParams.PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW;
lp.setTitle("ImmersiveModeConfirmation");
lp.windowAnimations = com.android.internal.R.style.Animation_ImmersiveModeConfirmation;
lp.token = getWindowToken();
@@ -469,6 +471,9 @@ public class ImmersiveModeConfirmation {
@Override
public void handleMessage(Message msg) {
+ if (CLIENT_TRANSIENT) {
+ return;
+ }
switch(msg.what) {
case SHOW:
handleShow(msg.arg1);
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index e945bc1babd9..9e3a611c0e70 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -85,6 +85,11 @@ final class LetterboxConfiguration {
// TODO(b/288142656): Enable user aspect ratio settings by default.
private static final boolean DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_SETTINGS = false;
+ // Whether per-app fullscreen user aspect ratio override option is enabled
+ private static final String KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN =
+ "enable_app_compat_user_aspect_ratio_fullscreen";
+ private static final boolean DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_FULLSCREEN = true;
+
// Whether the letterbox wallpaper style is enabled by default
private static final String KEY_ENABLE_LETTERBOX_BACKGROUND_WALLPAPER =
"enable_letterbox_background_wallpaper";
@@ -266,6 +271,9 @@ final class LetterboxConfiguration {
// Allows to enable user aspect ratio settings ignoring flags.
private boolean mUserAppAspectRatioSettingsOverrideEnabled;
+ // Allows to enable fullscreen option in user aspect ratio settings ignoring flags.
+ private boolean mUserAppAspectRatioFullscreenOverrideEnabled;
+
// The override for letterbox background type in case it's different from
// LETTERBOX_BACKGROUND_OVERRIDE_UNSET
@LetterboxBackgroundType
@@ -379,6 +387,10 @@ final class LetterboxConfiguration {
R.bool.config_appCompatUserAppAspectRatioSettingsIsEnabled))
.addDeviceConfigEntry(KEY_ENABLE_LETTERBOX_BACKGROUND_WALLPAPER,
DEFAULT_VALUE_ENABLE_LETTERBOX_BACKGROUND_WALLPAPER, /* enabled */ true)
+ .addDeviceConfigEntry(KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN,
+ DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_FULLSCREEN,
+ mContext.getResources().getBoolean(
+ R.bool.config_appCompatUserAppAspectRatioFullscreenIsEnabled))
.build();
}
@@ -1275,4 +1287,21 @@ final class LetterboxConfiguration {
void resetUserAppAspectRatioSettingsEnabled() {
setUserAppAspectRatioSettingsOverrideEnabled(false);
}
+
+ /**
+ * Whether fullscreen option in per-app user aspect ratio settings is enabled
+ */
+ boolean isUserAppAspectRatioFullscreenEnabled() {
+ return isUserAppAspectRatioSettingsEnabled()
+ && (mUserAppAspectRatioFullscreenOverrideEnabled
+ || mDeviceConfig.getFlagValue(KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN));
+ }
+
+ void setUserAppAspectRatioFullscreenOverrideEnabled(boolean enabled) {
+ mUserAppAspectRatioFullscreenOverrideEnabled = enabled;
+ }
+
+ void resetUserAppAspectRatioFullscreenEnabled() {
+ setUserAppAspectRatioFullscreenOverrideEnabled(false);
+ }
}
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 37985ea0aa6a..1565341deb4c 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -578,6 +578,17 @@ class TransitionController {
}
/**
+ * Returns {@code true} if the window container is in the collecting transition, and its
+ * collected rotation is different from the target rotation.
+ */
+ boolean hasCollectingRotationChange(@NonNull WindowContainer<?> wc, int targetRotation) {
+ final Transition transition = mCollectingTransition;
+ if (transition == null || !transition.mParticipants.contains(wc)) return false;
+ final Transition.ChangeInfo changeInfo = transition.mChanges.get(wc);
+ return changeInfo != null && changeInfo.mRotation != targetRotation;
+ }
+
+ /**
* @see #requestTransitionIfNeeded(int, int, WindowContainer, WindowContainer, RemoteTransition)
*/
@Nullable
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index f4781f9bc9f0..ceebb27642ce 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -1013,6 +1013,10 @@ public class WindowManagerShellCommand extends ShellCommand {
runSetBooleanFlag(pw, mLetterboxConfiguration
::setUserAppAspectRatioSettingsOverrideEnabled);
break;
+ case "--isUserAppAspectRatioFullscreenEnabled":
+ runSetBooleanFlag(pw, mLetterboxConfiguration
+ ::setUserAppAspectRatioFullscreenOverrideEnabled);
+ break;
case "--isCameraCompatRefreshEnabled":
runSetBooleanFlag(pw, mLetterboxConfiguration::setCameraCompatRefreshEnabled);
break;
@@ -1093,6 +1097,9 @@ public class WindowManagerShellCommand extends ShellCommand {
case "isUserAppAspectRatioSettingsEnabled":
mLetterboxConfiguration.resetUserAppAspectRatioSettingsEnabled();
break;
+ case "isUserAppAspectRatioFullscreenEnabled":
+ mLetterboxConfiguration.resetUserAppAspectRatioFullscreenEnabled();
+ break;
case "isCameraCompatRefreshEnabled":
mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
break;
@@ -1204,6 +1211,7 @@ public class WindowManagerShellCommand extends ShellCommand {
mLetterboxConfiguration.resetIsDisplayAspectRatioEnabledForFixedOrientationLetterbox();
mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
mLetterboxConfiguration.resetUserAppAspectRatioSettingsEnabled();
+ mLetterboxConfiguration.resetUserAppAspectRatioFullscreenEnabled();
mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
mLetterboxConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled();
}
@@ -1272,6 +1280,8 @@ public class WindowManagerShellCommand extends ShellCommand {
+ mLetterboxConfiguration.isTranslucentLetterboxingEnabled());
pw.println("Is the user aspect ratio settings enabled: "
+ mLetterboxConfiguration.isUserAppAspectRatioSettingsEnabled());
+ pw.println("Is the fullscreen option in user aspect ratio settings enabled: "
+ + mLetterboxConfiguration.isUserAppAspectRatioFullscreenEnabled());
}
return 0;
}
@@ -1471,6 +1481,8 @@ public class WindowManagerShellCommand extends ShellCommand {
pw.println(" Whether letterboxing for translucent activities is enabled.");
pw.println(" --isUserAppAspectRatioSettingsEnabled [true|1|false|0]");
pw.println(" Whether user aspect ratio settings are enabled.");
+ pw.println(" --isUserAppAspectRatioFullscreenEnabled [true|1|false|0]");
+ pw.println(" Whether user aspect ratio fullscreen option is enabled.");
pw.println(" --isCameraCompatRefreshEnabled [true|1|false|0]");
pw.println(" Whether camera compatibility refresh is enabled.");
pw.println(" --isCameraCompatRefreshCycleThroughStopEnabled [true|1|false|0]");
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 0d4c2d631b2c..140255b2f016 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -5132,7 +5132,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
private void applyDims() {
if (((mAttrs.flags & FLAG_DIM_BEHIND) != 0 || shouldDrawBlurBehind())
- && isVisibleNow() && !mHidden && mTransitionController.canApplyDim(getTask())) {
+ && mToken.isVisibleRequested() && isVisibleNow() && !mHidden
+ && mTransitionController.canApplyDim(getTask())) {
// Only show the Dimmer when the following is satisfied:
// 1. The window has the flag FLAG_DIM_BEHIND or blur behind is requested
// 2. The WindowToken is not hidden so dims aren't shown when the window is exiting.
@@ -5142,7 +5143,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
mIsDimming = true;
final float dimAmount = (mAttrs.flags & FLAG_DIM_BEHIND) != 0 ? mAttrs.dimAmount : 0;
final int blurRadius = shouldDrawBlurBehind() ? mAttrs.getBlurBehindRadius() : 0;
- getDimmer().dimBelow(getSyncTransaction(), this, dimAmount, blurRadius);
+ getDimmer().dimBelow(this, dimAmount, blurRadius);
}
}
@@ -5702,8 +5703,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
// window becomes visible while the sync group is still active.
return true;
}
- if (mSyncState == SYNC_STATE_WAITING_FOR_DRAW && mWinAnimator.mDrawState == HAS_DRAWN
- && !mRedrawForSyncReported && !mWmService.mResizingWindows.contains(this)) {
+ if (mSyncState == SYNC_STATE_WAITING_FOR_DRAW && mLastConfigReportedToClient && isDrawn()) {
// Complete the sync state immediately for a drawn window that doesn't need to redraw.
onSyncFinishedDrawing();
}
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index c065cb5f4ebe..9d391654063c 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -108,6 +108,7 @@ static struct {
jmethodID notifySensorEvent;
jmethodID notifySensorAccuracy;
jmethodID notifyStylusGestureStarted;
+ jmethodID isInputMethodConnectionActive;
jmethodID notifyVibratorState;
jmethodID filterInputEvent;
jmethodID interceptKeyBeforeQueueing;
@@ -322,6 +323,7 @@ public:
TouchAffineTransformation getTouchAffineTransformation(JNIEnv* env, jfloatArray matrixArr);
void notifyStylusGestureStarted(int32_t deviceId, nsecs_t eventTime) override;
+ bool isInputMethodConnectionActive() override;
/* --- InputDispatcherPolicyInterface implementation --- */
@@ -1306,6 +1308,14 @@ void NativeInputManager::notifyStylusGestureStarted(int32_t deviceId, nsecs_t ev
checkAndClearExceptionFromCallback(env, "notifyStylusGestureStarted");
}
+bool NativeInputManager::isInputMethodConnectionActive() {
+ JNIEnv* env = jniEnv();
+ const jboolean result =
+ env->CallBooleanMethod(mServiceObj, gServiceClassInfo.isInputMethodConnectionActive);
+ checkAndClearExceptionFromCallback(env, "isInputMethodConnectionActive");
+ return result;
+}
+
bool NativeInputManager::filterInputEvent(const InputEvent& inputEvent, uint32_t policyFlags) {
ATRACE_CALL();
JNIEnv* env = jniEnv();
@@ -2743,6 +2753,9 @@ int register_android_server_InputManager(JNIEnv* env) {
GET_METHOD_ID(gServiceClassInfo.notifyStylusGestureStarted, clazz, "notifyStylusGestureStarted",
"(IJ)V");
+ GET_METHOD_ID(gServiceClassInfo.isInputMethodConnectionActive, clazz,
+ "isInputMethodConnectionActive", "()Z");
+
GET_METHOD_ID(gServiceClassInfo.notifyVibratorState, clazz, "notifyVibratorState", "(IZ)V");
GET_METHOD_ID(gServiceClassInfo.notifyNoFocusedWindowAnr, clazz, "notifyNoFocusedWindowAnr",
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index fe913b9807cf..f3c2de6f7af1 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -241,7 +241,6 @@ import static android.provider.Telephony.Carriers.ENFORCE_KEY;
import static android.provider.Telephony.Carriers.ENFORCE_MANAGED_URI;
import static android.provider.Telephony.Carriers.INVALID_APN_ID;
import static android.security.keystore.AttestationUtils.USE_INDIVIDUAL_ATTESTATION;
-
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.PROVISIONING_ENTRY_POINT_ADB;
import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW;
@@ -540,6 +539,8 @@ import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
@@ -18817,10 +18818,16 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
});
}
+ ThreadPoolExecutor calculateHasIncompatibleAccountsExecutor = new ThreadPoolExecutor(
+ 1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
+
@Override
public void calculateHasIncompatibleAccounts() {
+ if (calculateHasIncompatibleAccountsExecutor.getQueue().size() > 1) {
+ return;
+ }
new CalculateHasIncompatibleAccountsTask().executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, null);
+ calculateHasIncompatibleAccountsExecutor, null);
}
@Nullable
diff --git a/services/tests/displayservicetests/TEST_MAPPING b/services/tests/displayservicetests/TEST_MAPPING
index d8655194ab0f..477860d3a1c5 100644
--- a/services/tests/displayservicetests/TEST_MAPPING
+++ b/services/tests/displayservicetests/TEST_MAPPING
@@ -1,13 +1,7 @@
{
- "presubmit": [
+ "imports": [
{
- "name": "DisplayServiceTests",
- "options": [
- {"include-filter": "com.android.server.display"},
- {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
- {"exclude-annotation": "androidx.test.filters.FlakyTest"},
- {"exclude-annotation": "org.junit.Ignore"}
- ]
+ "path": "frameworks/base/services/core/java/com/android/server/display"
}
]
}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
index 56f650ee9084..c39bb56e7ba1 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
@@ -50,6 +50,7 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.PowerManager;
import android.os.SystemProperties;
+import android.os.UserHandle;
import android.os.test.TestLooper;
import android.provider.Settings;
import android.testing.TestableContext;
@@ -144,11 +145,12 @@ public final class DisplayPowerController2Test {
mTestLooper = new TestLooper(mClock::now);
mHandler = new Handler(mTestLooper.getLooper());
- // Put the system into manual brightness by default, just to minimize unexpected events and
- // have a consistent starting state
+ // Set some settings to minimize unexpected events and have a consistent starting state
Settings.System.putInt(mContext.getContentResolver(),
Settings.System.SCREEN_BRIGHTNESS_MODE,
Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
+ Settings.System.putFloatForUser(mContext.getContentResolver(),
+ Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0, UserHandle.USER_CURRENT);
addLocalServiceMock(WindowManagerPolicy.class, mWindowManagerPolicyMock);
addLocalServiceMock(ColorDisplayService.ColorDisplayServiceInternal.class,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index e2aeea3bedba..0544376959ee 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -50,6 +50,7 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.PowerManager;
import android.os.SystemProperties;
+import android.os.UserHandle;
import android.os.test.TestLooper;
import android.provider.Settings;
import android.testing.TestableContext;
@@ -144,12 +145,12 @@ public final class DisplayPowerControllerTest {
mTestLooper = new TestLooper(mClock::now);
mHandler = new Handler(mTestLooper.getLooper());
- // Put the system into manual brightness by default, just to minimize unexpected events and
- // have a consistent starting state
+ // Set some settings to minimize unexpected events and have a consistent starting state
Settings.System.putInt(mContext.getContentResolver(),
Settings.System.SCREEN_BRIGHTNESS_MODE,
Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
-
+ Settings.System.putFloatForUser(mContext.getContentResolver(),
+ Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0, UserHandle.USER_CURRENT);
addLocalServiceMock(WindowManagerPolicy.class, mWindowManagerPolicyMock);
addLocalServiceMock(ColorDisplayService.ColorDisplayServiceInternal.class,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java
index d9fbba5b4274..7e7ccf733876 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java
@@ -17,35 +17,69 @@
package com.android.server.display;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.junit.Before;
import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
public class HighBrightnessModeMetadataMapperTest {
+ @Mock
+ private LogicalDisplay mDisplayMock;
+
+ @Mock
+ private DisplayDevice mDeviceMock;
+
+ @Mock
+ private DisplayDeviceConfig mDdcMock;
+
+ @Mock
+ private DisplayDeviceConfig.HighBrightnessModeData mHbmDataMock;
+
private HighBrightnessModeMetadataMapper mHighBrightnessModeMetadataMapper;
@Before
public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mDisplayMock.getPrimaryDisplayDeviceLocked()).thenReturn(mDeviceMock);
+ when(mDeviceMock.getDisplayDeviceConfig()).thenReturn(mDdcMock);
+ when(mDdcMock.getHighBrightnessModeData()).thenReturn(mHbmDataMock);
mHighBrightnessModeMetadataMapper = new HighBrightnessModeMetadataMapper();
}
@Test
- public void testGetHighBrightnessModeMetadata() {
- // Display device is null
- final LogicalDisplay display = mock(LogicalDisplay.class);
- when(display.getPrimaryDisplayDeviceLocked()).thenReturn(null);
- assertNull(mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display));
-
- // No HBM metadata stored for this display yet
- final DisplayDevice device = mock(DisplayDevice.class);
- when(display.getPrimaryDisplayDeviceLocked()).thenReturn(device);
+ public void testGetHighBrightnessModeMetadata_NoDisplayDevice() {
+ when(mDisplayMock.getPrimaryDisplayDeviceLocked()).thenReturn(null);
+ assertNull(mHighBrightnessModeMetadataMapper
+ .getHighBrightnessModeMetadataLocked(mDisplayMock));
+ }
+
+ @Test
+ public void testGetHighBrightnessModeMetadata_NoHBMData() {
+ when(mDdcMock.getHighBrightnessModeData()).thenReturn(null);
+ assertNull(mHighBrightnessModeMetadataMapper
+ .getHighBrightnessModeMetadataLocked(mDisplayMock));
+ }
+
+ @Test
+ public void testGetHighBrightnessModeMetadata_NewDisplay() {
HighBrightnessModeMetadata hbmMetadata =
- mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display);
+ mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(mDisplayMock);
+ assertNotNull(hbmMetadata);
+ assertTrue(hbmMetadata.getHbmEventQueue().isEmpty());
+ assertTrue(hbmMetadata.getRunningStartTimeMillis() < 0);
+ }
+
+ @Test
+ public void testGetHighBrightnessModeMetadata_Modify() {
+ HighBrightnessModeMetadata hbmMetadata =
+ mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(mDisplayMock);
+ assertNotNull(hbmMetadata);
assertTrue(hbmMetadata.getHbmEventQueue().isEmpty());
assertTrue(hbmMetadata.getRunningStartTimeMillis() < 0);
@@ -55,8 +89,10 @@ public class HighBrightnessModeMetadataMapperTest {
long setTime = 300;
hbmMetadata.addHbmEvent(new HbmEvent(startTimeMillis, endTimeMillis));
hbmMetadata.setRunningStartTimeMillis(setTime);
+
hbmMetadata =
- mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display);
+ mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(mDisplayMock);
+
assertEquals(1, hbmMetadata.getHbmEventQueue().size());
assertEquals(startTimeMillis,
hbmMetadata.getHbmEventQueue().getFirst().getStartTimeMillis());
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index f1e26d2c7083..6b225fc945d5 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -405,6 +405,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
UriGrantsManagerInternal mUgmInternal;
@Mock
AppOpsManager mAppOpsManager;
+ private AppOpsManager.OnOpChangedListener mOnPermissionChangeListener;
@Mock
private TestableNotificationManagerService.NotificationAssistantAccessGrantedCallback
mNotificationAssistantAccessGrantedCallback;
@@ -605,6 +606,13 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
tr.addOverride(com.android.internal.R.string.config_defaultSearchSelectorPackageName,
SEARCH_SELECTOR_PKG);
+ doAnswer(invocation -> {
+ mOnPermissionChangeListener = invocation.getArgument(2);
+ return null;
+ }).when(mAppOpsManager).startWatchingMode(eq(AppOpsManager.OP_POST_NOTIFICATION), any(),
+ any());
+ when(mUmInternal.isUserInitialized(anyInt())).thenReturn(true);
+
mWorkerHandler = spy(mService.new WorkerHandler(mTestableLooper.getLooper()));
mService.init(mWorkerHandler, mRankingHandler, mPackageManager, mPackageManagerClient,
mockLightsManager, mListeners, mAssistants, mConditionProviders, mCompanionMgr,
@@ -697,6 +705,11 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
mTestFlagResolver.setFlagOverride(FSI_FORCE_DEMOTE, false);
mTestFlagResolver.setFlagOverride(SHOW_STICKY_HUN_FOR_DENIED_FSI, false);
+
+ var checker = mock(TestableNotificationManagerService.ComponentPermissionChecker.class);
+ mService.permissionChecker = checker;
+ when(checker.check(anyString(), anyInt(), anyInt(), anyBoolean()))
+ .thenReturn(PackageManager.PERMISSION_DENIED);
}
@After
@@ -3221,6 +3234,108 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
+ public void onOpChanged_permissionRevoked_cancelsAllNotificationsFromPackage()
+ throws RemoteException {
+ // Have preexisting posted notifications from revoked package and other packages.
+ mService.addNotification(new NotificationRecord(mContext,
+ generateSbn("revoked", 1001, 1, 0), mTestNotificationChannel));
+ mService.addNotification(new NotificationRecord(mContext,
+ generateSbn("other", 1002, 2, 0), mTestNotificationChannel));
+ // Have preexisting enqueued notifications from revoked package and other packages.
+ mService.addEnqueuedNotification(new NotificationRecord(mContext,
+ generateSbn("revoked", 1001, 3, 0), mTestNotificationChannel));
+ mService.addEnqueuedNotification(new NotificationRecord(mContext,
+ generateSbn("other", 1002, 4, 0), mTestNotificationChannel));
+ assertThat(mService.mNotificationList).hasSize(2);
+ assertThat(mService.mEnqueuedNotifications).hasSize(2);
+
+ when(mPackageManagerInternal.getPackageUid("revoked", 0, 0)).thenReturn(1001);
+ when(mPermissionHelper.hasPermission(eq(1001))).thenReturn(false);
+
+ mOnPermissionChangeListener.onOpChanged(
+ AppOpsManager.OPSTR_POST_NOTIFICATION, "revoked", 0);
+ waitForIdle();
+
+ assertThat(mService.mNotificationList).hasSize(1);
+ assertThat(mService.mNotificationList.get(0).getSbn().getPackageName()).isEqualTo("other");
+ assertThat(mService.mEnqueuedNotifications).hasSize(1);
+ assertThat(mService.mEnqueuedNotifications.get(0).getSbn().getPackageName()).isEqualTo(
+ "other");
+ }
+
+ @Test
+ public void onOpChanged_permissionStillGranted_notificationsAreNotAffected()
+ throws RemoteException {
+ // NOTE: This combination (receiving the onOpChanged broadcast for a package, the permission
+ // being now granted, AND having previously posted notifications from said package) should
+ // never happen (if we trust the broadcasts are correct). So this test is for a what-if
+ // scenario, to verify we still handle it reasonably.
+
+ // Have preexisting posted notifications from specific package and other packages.
+ mService.addNotification(new NotificationRecord(mContext,
+ generateSbn("granted", 1001, 1, 0), mTestNotificationChannel));
+ mService.addNotification(new NotificationRecord(mContext,
+ generateSbn("other", 1002, 2, 0), mTestNotificationChannel));
+ // Have preexisting enqueued notifications from specific package and other packages.
+ mService.addEnqueuedNotification(new NotificationRecord(mContext,
+ generateSbn("granted", 1001, 3, 0), mTestNotificationChannel));
+ mService.addEnqueuedNotification(new NotificationRecord(mContext,
+ generateSbn("other", 1002, 4, 0), mTestNotificationChannel));
+ assertThat(mService.mNotificationList).hasSize(2);
+ assertThat(mService.mEnqueuedNotifications).hasSize(2);
+
+ when(mPackageManagerInternal.getPackageUid("granted", 0, 0)).thenReturn(1001);
+ when(mPermissionHelper.hasPermission(eq(1001))).thenReturn(true);
+
+ mOnPermissionChangeListener.onOpChanged(
+ AppOpsManager.OPSTR_POST_NOTIFICATION, "granted", 0);
+ waitForIdle();
+
+ assertThat(mService.mNotificationList).hasSize(2);
+ assertThat(mService.mEnqueuedNotifications).hasSize(2);
+ }
+
+ @Test
+ public void onOpChanged_notInitializedUser_ignored() throws RemoteException {
+ when(mUmInternal.isUserInitialized(eq(0))).thenReturn(false);
+
+ mOnPermissionChangeListener.onOpChanged(
+ AppOpsManager.OPSTR_POST_NOTIFICATION, "package", 0);
+ waitForIdle();
+
+ // We early-exited and didn't even query PM for package details.
+ verify(mPackageManagerInternal, never()).getPackageUid(any(), anyLong(), anyInt());
+ }
+
+ @Test
+ public void setNotificationsEnabledForPackage_disabling_clearsNotifications() throws Exception {
+ mService.addNotification(new NotificationRecord(mContext,
+ generateSbn("package", 1001, 1, 0), mTestNotificationChannel));
+ assertThat(mService.mNotificationList).hasSize(1);
+ when(mPackageManagerInternal.getPackageUid("package", 0, 0)).thenReturn(1001);
+ when(mPermissionHelper.hasRequestedPermission(any(), eq("package"), anyInt())).thenReturn(
+ true);
+
+ // Start with granted permission and simulate effect of revoking it.
+ when(mPermissionHelper.hasPermission(1001)).thenReturn(true);
+ doAnswer(invocation -> {
+ when(mPermissionHelper.hasPermission(1001)).thenReturn(false);
+ mOnPermissionChangeListener.onOpChanged(
+ AppOpsManager.OPSTR_POST_NOTIFICATION, "package", 0);
+ return null;
+ }).when(mPermissionHelper).setNotificationPermission("package", 0, false, true);
+
+ mBinderService.setNotificationsEnabledForPackage("package", 1001, false);
+ waitForIdle();
+
+ assertThat(mService.mNotificationList).hasSize(0);
+
+ mTestableLooper.moveTimeForward(500);
+ waitForIdle();
+ verify(mContext).sendBroadcastAsUser(any(), eq(UserHandle.of(0)), eq(null));
+ }
+
+ @Test
public void testUpdateAppNotifyCreatorBlock() throws Exception {
when(mPermissionHelper.hasPermission(mUid)).thenReturn(true);
@@ -4240,6 +4355,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
@Test
public void testSetNASMigrationDoneAndResetDefault_enableNAS() throws Exception {
int userId = 10;
+ setNASMigrationDone(false, userId);
when(mUm.getProfileIds(userId, false)).thenReturn(new int[]{userId});
mBinderService.setNASMigrationDoneAndResetDefault(userId, true);
@@ -4251,6 +4367,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
@Test
public void testSetNASMigrationDoneAndResetDefault_disableNAS() throws Exception {
int userId = 10;
+ setNASMigrationDone(false, userId);
when(mUm.getProfileIds(userId, false)).thenReturn(new int[]{userId});
mBinderService.setNASMigrationDoneAndResetDefault(userId, false);
@@ -4263,6 +4380,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
public void testSetNASMigrationDoneAndResetDefault_multiProfile() throws Exception {
int userId1 = 11;
int userId2 = 12; //work profile
+ setNASMigrationDone(false, userId1);
+ setNASMigrationDone(false, userId2);
setUsers(new int[]{userId1, userId2});
when(mUm.isManagedProfile(userId2)).thenReturn(true);
when(mUm.getProfileIds(userId1, false)).thenReturn(new int[]{userId1, userId2});
@@ -4276,6 +4395,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
public void testSetNASMigrationDoneAndResetDefault_multiUser() throws Exception {
int userId1 = 11;
int userId2 = 12;
+ setNASMigrationDone(false, userId1);
+ setNASMigrationDone(false, userId2);
setUsers(new int[]{userId1, userId2});
when(mUm.getProfileIds(userId1, false)).thenReturn(new int[]{userId1});
when(mUm.getProfileIds(userId2, false)).thenReturn(new int[]{userId2});
@@ -12168,6 +12289,130 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
inOrder.verifyNoMoreInteractions();
}
+ @Test
+ public void isNotificationPolicyAccessGranted_invalidPackage() throws Exception {
+ final String notReal = "NOT REAL";
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(notReal), anyInt())).thenThrow(
+ PackageManager.NameNotFoundException.class);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(notReal)).isFalse();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(notReal), anyInt());
+ verify(checker, never()).check(any(), anyInt(), anyInt(), anyBoolean());
+ verify(mConditionProviders, never()).isPackageOrComponentAllowed(eq(notReal), anyInt());
+ verify(mListeners, never()).isComponentEnabledForPackage(any());
+ verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt());
+ }
+
+ @Test
+ public void isNotificationPolicyAccessGranted_hasPermission() throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+ when(checker.check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true))
+ .thenReturn(PackageManager.PERMISSION_GRANTED);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders, never()).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners, never()).isComponentEnabledForPackage(any());
+ verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt());
+ }
+
+ @Test
+ public void isNotificationPolicyAccessGranted_isPackageAllowed() throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+ when(mConditionProviders.isPackageOrComponentAllowed(eq(packageName), anyInt()))
+ .thenReturn(true);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners, never()).isComponentEnabledForPackage(any());
+ verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt());
+ }
+
+ @Test
+ public void isNotificationPolicyAccessGranted_isComponentEnabled() throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+ when(mListeners.isComponentEnabledForPackage(packageName)).thenReturn(true);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners).isComponentEnabledForPackage(packageName);
+ verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt());
+ }
+
+ @Test
+ public void isNotificationPolicyAccessGranted_isDeviceOwner() throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+ when(mDevicePolicyManager.isActiveDeviceOwner(uid)).thenReturn(true);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners).isComponentEnabledForPackage(packageName);
+ verify(mDevicePolicyManager).isActiveDeviceOwner(uid);
+ }
+
+ /**
+ * b/292163859
+ */
+ @Test
+ public void isNotificationPolicyAccessGranted_callerIsDeviceOwner() throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final int callingUid = Binder.getCallingUid();
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+ when(mDevicePolicyManager.isActiveDeviceOwner(callingUid)).thenReturn(true);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isFalse();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners).isComponentEnabledForPackage(packageName);
+ verify(mDevicePolicyManager).isActiveDeviceOwner(uid);
+ verify(mDevicePolicyManager, never()).isActiveDeviceOwner(callingUid);
+ }
+
+ @Test
+ public void isNotificationPolicyAccessGranted_notGranted() throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isFalse();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners).isComponentEnabledForPackage(packageName);
+ verify(mDevicePolicyManager).isActiveDeviceOwner(uid);
+ }
+
private static <T extends Parcelable> T parcelAndUnparcel(T source,
Parcelable.Creator<T> creator) {
Parcel parcel = Parcel.obtain();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
index 9f4eee7e332f..27e8f3664a65 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
@@ -43,6 +43,8 @@ public class TestableNotificationManagerService extends NotificationManagerServi
@Nullable
Boolean mIsVisibleToListenerReturnValue = null;
+ ComponentPermissionChecker permissionChecker;
+
TestableNotificationManagerService(Context context, NotificationRecordLogger logger,
InstanceIdSequence notificationInstanceIdSequence) {
super(context, logger, notificationInstanceIdSequence);
@@ -150,6 +152,12 @@ public class TestableNotificationManagerService extends NotificationManagerServi
return super.isVisibleToListener(sbn, notificationType, listener);
}
+ @Override
+ protected int checkComponentPermission(String permission, int uid, int owningUid,
+ boolean exported) {
+ return permissionChecker.check(permission, uid, owningUid, exported);
+ }
+
public class StrongAuthTrackerFake extends NotificationManagerService.StrongAuthTracker {
private int mGetStrongAuthForUserReturnValue = 0;
StrongAuthTrackerFake(Context context) {
@@ -165,4 +173,8 @@ public class TestableNotificationManagerService extends NotificationManagerServi
return mGetStrongAuthForUserReturnValue;
}
}
+
+ public interface ComponentPermissionChecker {
+ int check(String permission, int uid, int owningUid, boolean exported);
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
index f235d153c658..233a2076a867 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
@@ -52,7 +52,8 @@ public class DimmerTests extends WindowTestsBase {
private static class TestWindowContainer extends WindowContainer<TestWindowContainer> {
final SurfaceControl mControl = mock(SurfaceControl.class);
- final SurfaceControl.Transaction mTransaction = spy(StubTransaction.class);
+ final SurfaceControl.Transaction mPendingTransaction = spy(StubTransaction.class);
+ final SurfaceControl.Transaction mSyncTransaction = spy(StubTransaction.class);
TestWindowContainer(WindowManagerService wm) {
super(wm);
@@ -65,12 +66,12 @@ public class DimmerTests extends WindowTestsBase {
@Override
public SurfaceControl.Transaction getSyncTransaction() {
- return mTransaction;
+ return mSyncTransaction;
}
@Override
public SurfaceControl.Transaction getPendingTransaction() {
- return mTransaction;
+ return mPendingTransaction;
}
}
@@ -144,7 +145,7 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final float alpha = 0.8f;
- mDimmer.dimAbove(mTransaction, child, alpha);
+ mDimmer.dimAbove(child, alpha);
int width = 100;
int height = 300;
@@ -161,13 +162,13 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final float alpha = 0.8f;
- mDimmer.dimAbove(mTransaction, child, alpha);
+ mDimmer.dimAbove(child, alpha);
SurfaceControl dimLayer = getDimLayer();
assertNotNull("Dimmer should have created a surface", dimLayer);
- verify(mTransaction).setAlpha(dimLayer, alpha);
- verify(mTransaction).setRelativeLayer(dimLayer, child.mControl, 1);
+ verify(mHost.getPendingTransaction()).setAlpha(dimLayer, alpha);
+ verify(mHost.getPendingTransaction()).setRelativeLayer(dimLayer, child.mControl, 1);
}
@Test
@@ -176,13 +177,13 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final float alpha = 0.8f;
- mDimmer.dimBelow(mTransaction, child, alpha, 0);
+ mDimmer.dimBelow(child, alpha, 0);
SurfaceControl dimLayer = getDimLayer();
assertNotNull("Dimmer should have created a surface", dimLayer);
- verify(mTransaction).setAlpha(dimLayer, alpha);
- verify(mTransaction).setRelativeLayer(dimLayer, child.mControl, -1);
+ verify(mHost.getPendingTransaction()).setAlpha(dimLayer, alpha);
+ verify(mHost.getPendingTransaction()).setRelativeLayer(dimLayer, child.mControl, -1);
}
@Test
@@ -191,7 +192,7 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final float alpha = 0.8f;
- mDimmer.dimAbove(mTransaction, child, alpha);
+ mDimmer.dimAbove(child, alpha);
SurfaceControl dimLayer = getDimLayer();
mDimmer.resetDimStates();
@@ -208,10 +209,10 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final float alpha = 0.8f;
- mDimmer.dimAbove(mTransaction, child, alpha);
+ mDimmer.dimAbove(child, alpha);
SurfaceControl dimLayer = getDimLayer();
mDimmer.resetDimStates();
- mDimmer.dimAbove(mTransaction, child, alpha);
+ mDimmer.dimAbove(child, alpha);
mDimmer.updateDims(mTransaction);
verify(mTransaction).show(dimLayer);
@@ -224,7 +225,7 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final float alpha = 0.8f;
- mDimmer.dimAbove(mTransaction, child, alpha);
+ mDimmer.dimAbove(child, alpha);
final Rect bounds = mDimmer.mDimState.mDimBounds;
SurfaceControl dimLayer = getDimLayer();
@@ -245,7 +246,7 @@ public class DimmerTests extends WindowTestsBase {
TestWindowContainer child = new TestWindowContainer(mWm);
mHost.addChild(child, 0);
- mDimmer.dimAbove(mTransaction, child, 1);
+ mDimmer.dimAbove(child, 1);
SurfaceControl dimLayer = getDimLayer();
mDimmer.updateDims(mTransaction);
verify(mTransaction, times(1)).show(dimLayer);
@@ -266,13 +267,13 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final int blurRadius = 50;
- mDimmer.dimBelow(mTransaction, child, 0, blurRadius);
+ mDimmer.dimBelow(child, 0, blurRadius);
SurfaceControl dimLayer = getDimLayer();
assertNotNull("Dimmer should have created a surface", dimLayer);
- verify(mTransaction).setBackgroundBlurRadius(dimLayer, blurRadius);
- verify(mTransaction).setRelativeLayer(dimLayer, child.mControl, -1);
+ verify(mHost.getPendingTransaction()).setBackgroundBlurRadius(dimLayer, blurRadius);
+ verify(mHost.getPendingTransaction()).setRelativeLayer(dimLayer, child.mControl, -1);
}
private SurfaceControl getDimLayer() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
index fc5e9cab5447..810cbe8f8080 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
@@ -40,9 +40,7 @@ import static org.mockito.ArgumentMatchers.notNull;
import static org.mockito.Mockito.spy;
import android.platform.test.annotations.Presubmit;
-import android.util.MergedConfiguration;
import android.view.SurfaceControl;
-import android.window.ClientWindowFrames;
import androidx.test.filters.SmallTest;
@@ -333,8 +331,7 @@ public class SyncEngineTests extends WindowTestsBase {
w.reparent(botChildWC, POSITION_TOP);
parentWC.prepareSync();
// Assume the window has drawn with the latest configuration.
- w.fillClientWindowFramesAndConfiguration(new ClientWindowFrames(),
- new MergedConfiguration(), true /* useLatestConfig */, true /* relayoutVisible */);
+ makeLastConfigReportedToClient(w, true /* visible */);
assertTrue(w.onSyncFinishedDrawing());
assertEquals(SYNC_STATE_READY, w.mSyncState);
w.reparent(topChildWC, POSITION_TOP);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index e91fdde955ef..ca5d8fe33dba 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -1240,6 +1240,27 @@ public class TransitionTests extends WindowTestsBase {
}
@Test
+ public void testFinishRotationControllerWithFixedRotation() {
+ final ActivityRecord app = new ActivityBuilder(mAtm).setCreateTask(true).build();
+ mDisplayContent.setFixedRotationLaunchingAppUnchecked(app);
+ registerTestTransitionPlayer();
+ mDisplayContent.setLastHasContent();
+ mDisplayContent.requestChangeTransitionIfNeeded(1 /* changes */, null /* displayChange */);
+ assertNotNull(mDisplayContent.getAsyncRotationController());
+ mDisplayContent.setFixedRotationLaunchingAppUnchecked(null);
+ assertNull("Clear rotation controller if rotation is not changed",
+ mDisplayContent.getAsyncRotationController());
+
+ mDisplayContent.setFixedRotationLaunchingAppUnchecked(app);
+ assertNotNull(mDisplayContent.getAsyncRotationController());
+ mDisplayContent.getDisplayRotation().setRotation(
+ mDisplayContent.getWindowConfiguration().getRotation() + 1);
+ mDisplayContent.setFixedRotationLaunchingAppUnchecked(null);
+ assertNotNull("Keep rotation controller if rotation will be changed",
+ mDisplayContent.getAsyncRotationController());
+ }
+
+ @Test
public void testDeferRotationForTransientLaunch() {
final TestTransitionPlayer player = registerTestTransitionPlayer();
assumeFalse(mDisplayContent.mTransitionController.useShellTransitionsRotation());
diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
index 4afcd0539b94..e3d1b9c669f9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
@@ -52,7 +52,6 @@ import android.graphics.Rect;
import android.os.IBinder;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
-import android.util.MergedConfiguration;
import android.view.DisplayCutout;
import android.view.DisplayInfo;
import android.view.DisplayShape;
@@ -63,7 +62,6 @@ import android.view.RoundedCorners;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.WindowManager;
-import android.window.ClientWindowFrames;
import androidx.test.filters.SmallTest;
@@ -380,8 +378,7 @@ public class WallpaperControllerTests extends WindowTestsBase {
wallpaperWindow.mLayoutSeq = mDisplayContent.mLayoutSeq;
// Assume the token was invisible and the latest config was reported.
wallpaperToken.commitVisibility(false);
- wallpaperWindow.fillClientWindowFramesAndConfiguration(new ClientWindowFrames(),
- new MergedConfiguration(), true /* useLatestConfig */, false /* relayoutVisible */);
+ makeLastConfigReportedToClient(wallpaperWindow, false /* visible */);
assertTrue(wallpaperWindow.isLastConfigReportedToClient());
final Rect bounds = wallpaperToken.getBounds();
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index 600681fb332c..7168670f9652 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -1247,6 +1247,7 @@ public class WindowOrganizerTests extends WindowTestsBase {
// A drawn window can complete the sync state automatically.
w1.mWinAnimator.mDrawState = WindowStateAnimator.HAS_DRAWN;
+ makeLastConfigReportedToClient(w1, true /* visible */);
mWm.mSyncEngine.onSurfacePlacement();
verify(mockCallback).onTransactionReady(anyInt(), any());
assertFalse(w1.useBLASTSync());
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index d5547ec69247..873c7f4e0e30 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -85,6 +85,7 @@ import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.voice.IVoiceInteractionSession;
+import android.util.MergedConfiguration;
import android.util.SparseArray;
import android.view.Display;
import android.view.DisplayInfo;
@@ -102,6 +103,7 @@ import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowManager.DisplayImePolicy;
import android.view.inputmethod.ImeTracker;
+import android.window.ClientWindowFrames;
import android.window.ITransitionPlayer;
import android.window.ScreenCapture;
import android.window.StartingWindowInfo;
@@ -624,6 +626,11 @@ class WindowTestsBase extends SystemServiceTestsBase {
}
}
+ static void makeLastConfigReportedToClient(WindowState w, boolean visible) {
+ w.fillClientWindowFramesAndConfiguration(new ClientWindowFrames(),
+ new MergedConfiguration(), true /* useLatestConfig */, visible);
+ }
+
/**
* Gets the order of the given {@link Task} as its z-order in the hierarchy below this TDA.
* The Task can be a direct child of a child TaskDisplayArea. {@code -1} if not found.
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index 248cc26ce656..ccc4ac28876a 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -27,6 +27,7 @@ import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPH
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER;
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__SERVICE_CRASH;
+import android.app.AppOpsManager;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.ChangeId;
@@ -548,13 +549,15 @@ final class HotwordDetectionConnection {
static final class SoundTriggerCallback extends IRecognitionStatusCallback.Stub {
private final HotwordDetectionConnection mHotwordDetectionConnection;
private final IHotwordRecognitionStatusCallback mExternalCallback;
- private final int mVoiceInteractionServiceUid;
+ private final Identity mVoiceInteractorIdentity;
+ private final Context mContext;
- SoundTriggerCallback(IHotwordRecognitionStatusCallback callback,
- HotwordDetectionConnection connection, int uid) {
+ SoundTriggerCallback(Context context, IHotwordRecognitionStatusCallback callback,
+ HotwordDetectionConnection connection, Identity voiceInteractorIdentity) {
+ mContext = context;
mHotwordDetectionConnection = connection;
mExternalCallback = callback;
- mVoiceInteractionServiceUid = uid;
+ mVoiceInteractorIdentity = voiceInteractorIdentity;
}
@Override
@@ -568,15 +571,30 @@ final class HotwordDetectionConnection {
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP,
HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER,
- mVoiceInteractionServiceUid);
+ mVoiceInteractorIdentity.uid);
mHotwordDetectionConnection.detectFromDspSource(
recognitionEvent, mExternalCallback);
} else {
- HotwordMetricsLogger.writeKeyphraseTriggerEvent(
- HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__NORMAL_DETECTOR,
- HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER,
- mVoiceInteractionServiceUid);
- mExternalCallback.onKeyphraseDetected(recognitionEvent, null);
+ // We have to attribute ops here, since we configure all st clients as trusted to
+ // enable a partial exemption.
+ // TODO (b/292012931) remove once trusted uniformly required.
+ int result = mContext.getSystemService(AppOpsManager.class)
+ .noteOpNoThrow(AppOpsManager.OP_RECORD_AUDIO_HOTWORD,
+ mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName,
+ mVoiceInteractorIdentity.attributionTag,
+ "Non-HDS keyphrase recognition to VoiceInteractionService");
+
+ if (result != AppOpsManager.MODE_ALLOWED) {
+ Slog.w(TAG, "onKeyphraseDetected suppressed, permission check returned: "
+ + result);
+ mExternalCallback.onRecognitionPaused();
+ } else {
+ HotwordMetricsLogger.writeKeyphraseTriggerEvent(
+ HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__NORMAL_DETECTOR,
+ HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER,
+ mVoiceInteractorIdentity.uid);
+ mExternalCallback.onKeyphraseDetected(recognitionEvent, null);
+ }
}
}
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 423a81ac0523..3574ef8e91fb 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -104,6 +104,7 @@ import com.android.server.SystemService;
import com.android.server.UiThread;
import com.android.server.pm.UserManagerInternal;
import com.android.server.pm.permission.LegacyPermissionManagerInternal;
+import com.android.server.policy.AppOpsPolicy;
import com.android.server.utils.Slogf;
import com.android.server.utils.TimingsTraceAndSlog;
import com.android.server.wm.ActivityTaskManagerInternal;
@@ -336,6 +337,9 @@ public class VoiceInteractionManagerService extends SystemService {
/** The start value of showSessionId */
private static final int SHOW_SESSION_START_ID = 0;
+ private final boolean IS_HDS_REQUIRED = AppOpsPolicy.isHotwordDetectionServiceRequired(
+ mContext.getPackageManager());
+
@GuardedBy("this")
private int mShowSessionId = SHOW_SESSION_START_ID;
@@ -393,8 +397,14 @@ public class VoiceInteractionManagerService extends SystemService {
}
try (SafeCloseable ignored = PermissionUtil.establishIdentityDirect(
originatorIdentity)) {
+ if (!IS_HDS_REQUIRED) {
+ // For devices which still have hotword exemption, any client (not just HDS
+ // clients) are trusted.
+ // TODO (b/292012931) remove once trusted uniformly required.
+ forHotwordDetectionService = true;
+ }
return new SoundTriggerSession(mSoundTriggerInternal.attach(client,
- moduleProperties, forHotwordDetectionService));
+ moduleProperties, forHotwordDetectionService), originatorIdentity);
}
}
@@ -1674,10 +1684,13 @@ public class VoiceInteractionManagerService extends SystemService {
final SoundTriggerInternal.Session mSession;
private IHotwordRecognitionStatusCallback mSessionExternalCallback;
private IRecognitionStatusCallback mSessionInternalCallback;
+ private final Identity mVoiceInteractorIdentity;
SoundTriggerSession(
- SoundTriggerInternal.Session session) {
+ SoundTriggerInternal.Session session,
+ Identity voiceInteractorIdentity) {
mSession = session;
+ mVoiceInteractorIdentity = voiceInteractorIdentity;
}
@Override
@@ -1731,7 +1744,8 @@ public class VoiceInteractionManagerService extends SystemService {
if (mSessionExternalCallback == null
|| mSessionInternalCallback == null
|| callback.asBinder() != mSessionExternalCallback.asBinder()) {
- mSessionInternalCallback = createSoundTriggerCallbackLocked(callback);
+ mSessionInternalCallback = createSoundTriggerCallbackLocked(callback,
+ mVoiceInteractorIdentity);
mSessionExternalCallback = callback;
}
}
@@ -1752,7 +1766,8 @@ public class VoiceInteractionManagerService extends SystemService {
if (mSessionExternalCallback == null
|| mSessionInternalCallback == null
|| callback.asBinder() != mSessionExternalCallback.asBinder()) {
- soundTriggerCallback = createSoundTriggerCallbackLocked(callback);
+ soundTriggerCallback = createSoundTriggerCallbackLocked(callback,
+ mVoiceInteractorIdentity);
Slog.w(TAG, "stopRecognition() called with a different callback than"
+ "startRecognition()");
} else {
@@ -2090,6 +2105,7 @@ public class VoiceInteractionManagerService extends SystemService {
pw.println(" mTemporarilyDisabled: " + mTemporarilyDisabled);
pw.println(" mCurUser: " + mCurUser);
pw.println(" mCurUserSupported: " + mCurUserSupported);
+ pw.println(" mIsHdsRequired: " + IS_HDS_REQUIRED);
dumpSupportedUsers(pw, " ");
mDbHelper.dump(pw);
if (mImpl == null) {
@@ -2165,11 +2181,13 @@ public class VoiceInteractionManagerService extends SystemService {
}
private IRecognitionStatusCallback createSoundTriggerCallbackLocked(
- IHotwordRecognitionStatusCallback callback) {
+ IHotwordRecognitionStatusCallback callback,
+ Identity voiceInteractorIdentity) {
if (mImpl == null) {
return null;
}
- return mImpl.createSoundTriggerCallbackLocked(callback);
+ return mImpl.createSoundTriggerCallbackLocked(mContext, callback,
+ voiceInteractorIdentity);
}
class RoleObserver implements OnRoleHoldersChangedListener {
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index 0ad86c11d29a..5d88a65ce29e 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -877,12 +877,13 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne
}
public IRecognitionStatusCallback createSoundTriggerCallbackLocked(
- IHotwordRecognitionStatusCallback callback) {
+ Context context, IHotwordRecognitionStatusCallback callback,
+ Identity voiceInteractorIdentity) {
if (DEBUG) {
Slog.d(TAG, "createSoundTriggerCallbackLocked");
}
- return new HotwordDetectionConnection.SoundTriggerCallback(callback,
- mHotwordDetectionConnection, mInfo.getServiceInfo().applicationInfo.uid);
+ return new HotwordDetectionConnection.SoundTriggerCallback(context, callback,
+ mHotwordDetectionConnection, voiceInteractorIdentity);
}
private static ServiceInfo getServiceInfoLocked(@NonNull ComponentName componentName,
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 64e43568e4d6..8e4ec0914563 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -151,6 +151,15 @@ public class SubscriptionManager {
"restoreSimSpecificSettings";
/**
+ * The key of the boolean flag indicating whether restoring subscriptions actually changes
+ * the subscription database or not.
+ *
+ * @hide
+ */
+ public static final String RESTORE_SIM_SPECIFIC_SETTINGS_DATABASE_UPDATED =
+ "restoreSimSpecificSettingsDatabaseUpdated";
+
+ /**
* Key to the backup & restore data byte array in the Bundle that is returned by {@link
* #getAllSimSpecificSettingsForBackup()} or to be pass in to {@link
* #restoreAllSimSpecificSettingsFromBackup(byte[])}.
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt
index 45cd65d9776c..45176448a9f4 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt
@@ -17,9 +17,12 @@
package com.android.server.wm.flicker.activityembedding
import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.traces.component.ComponentNameMatcher
import com.android.server.wm.flicker.BaseTest
import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
import org.junit.Before
+import org.junit.Test
abstract class ActivityEmbeddingTestBase(flicker: LegacyFlickerTest) : BaseTest(flicker) {
val testApp = ActivityEmbeddingAppHelper(instrumentation)
@@ -29,4 +32,14 @@ abstract class ActivityEmbeddingTestBase(flicker: LegacyFlickerTest) : BaseTest(
// The test should only be run on devices that support ActivityEmbedding.
ActivityEmbeddingAppHelper.assumeActivityEmbeddingSupportedDevice()
}
+
+ /** Asserts the background animation layer is never visible during bounds change transition. */
+ @Presubmit
+ @Test
+ fun backgroundLayerNeverVisible() {
+ val backgroundColorLayer = ComponentNameMatcher("", "Animation Background")
+ flicker.assertLayers {
+ isInvisible(backgroundColorLayer)
+ }
+ }
}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt
new file mode 100644
index 000000000000..badd876ae321
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023 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.activityembedding
+
+import android.platform.test.annotations.Presubmit
+import android.tools.common.datatypes.Rect
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
+import androidx.test.filters.RequiresDevice
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test changing split ratio at runtime on a horizona split.
+ *
+ * Setup: Launch A|B in horizontal split with B being the secondary activity, by default A and B
+ * windows are equal in size. B is on the top and A is on the bottom.
+ * Transitions:
+ * Change the split ratio to A:B=0.7:0.3, expect bounds change for both A and B.
+ *
+ * To run this test: `atest FlickerTests:HorizontalSplitChangeRatioTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class HorizontalSplitChangeRatioTest(flicker: LegacyFlickerTest) :
+ ActivityEmbeddingTestBase(flicker) {
+ /** {@inheritDoc} */
+ override val transition: FlickerBuilder.() -> Unit = {
+ setup {
+ tapl.setExpectedRotationCheckEnabled(false)
+ testApp.launchViaIntent(wmHelper)
+ testApp.launchSecondaryActivityHorizontally(wmHelper)
+ startDisplayBounds =
+ wmHelper.currentState.layerState.physicalDisplayBounds
+ ?: error("Display not found")
+ }
+ transitions {
+ testApp.changeSecondaryActivityRatio(wmHelper)
+ }
+ teardown {
+ tapl.goHome()
+ testApp.exit(wmHelper)
+ }
+ }
+
+ /** Assert the Main activity window is always visible. */
+ @Presubmit
+ @Test
+ fun mainActivityWindowIsAlwaysVisible() {
+ flicker.assertWm { isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) }
+ }
+
+ /** Assert the Main activity window is always visible. */
+ @Presubmit
+ @Test
+ fun mainActivityLayerIsAlwaysVisible() {
+ flicker.assertLayers { isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) }
+ }
+
+ /** Assert the Secondary activity window is always visible. */
+ @Presubmit
+ @Test
+ fun secondaryActivityWindowIsAlwaysVisible() {
+ flicker.assertWm {
+ isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) }
+ }
+
+ /** Assert the Secondary activity window is always visible. */
+ @Presubmit
+ @Test
+ fun secondaryActivityLayerIsAlwaysVisible() {
+ flicker.assertLayers { isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) }
+ }
+
+ /** Assert the Main and Secondary activity change height during the transition. */
+ @Presubmit
+ @Test
+ fun secondaryActivityAdjustsHeightRuntime() {
+ flicker.assertLayersStart {
+ val topLayerRegion =
+ this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ val bottomLayerRegion =
+ this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ // Compare dimensions of two splits, given we're using default split attributes,
+ // both activities take up the same visible size on the display.
+ check { "height" }
+ .that(topLayerRegion.region.height).isEqual(bottomLayerRegion.region.height)
+ check { "width" }
+ .that(topLayerRegion.region.width).isEqual(bottomLayerRegion.region.width)
+ topLayerRegion.notOverlaps(bottomLayerRegion.region)
+ // Layers of two activities sum to be fullscreen size on display.
+ topLayerRegion.plus(bottomLayerRegion.region).coversExactly(startDisplayBounds)
+ }
+
+ flicker.assertLayersEnd {
+ val topLayerRegion =
+ this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ val bottomLayerRegion =
+ this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ // Compare dimensions of two splits, given we're using default split attributes,
+ // both activities take up the same visible size on the display.
+ check { "height" }
+ .that(topLayerRegion.region.height).isLower(bottomLayerRegion.region.height)
+ check { "height" }
+ .that(
+ topLayerRegion.region.height / 0.3f -
+ bottomLayerRegion.region.height / 0.7f)
+ .isLower(0.1f)
+ check { "width" }
+ .that(topLayerRegion.region.width).isEqual(bottomLayerRegion.region.width)
+ topLayerRegion.notOverlaps(bottomLayerRegion.region)
+ // Layers of two activities sum to be fullscreen size on display.
+ topLayerRegion.plus(bottomLayerRegion.region).coversExactly(startDisplayBounds)
+ }
+ }
+
+ companion object {
+ /** {@inheritDoc} */
+ private var startDisplayBounds = Rect.EMPTY
+ /**
+ * Creates the test configurations.
+ *
+ * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+ }
+} \ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt
index 27de12e7dfdb..404f3290f04a 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt
@@ -142,14 +142,6 @@ class OpenThirdActivityOverSplitTest(flicker: LegacyFlickerTest) :
}
}
- /** Assert the background animation layer is never visible during transition. */
- @Presubmit
- @Test
- fun backgroundLayerNeverVisible() {
- val backgroundColorLayer = ComponentNameMatcher("", "Animation Background")
- flicker.assertLayers { isInvisible(backgroundColorLayer) }
- }
-
companion object {
/** {@inheritDoc} */
private var startDisplayBounds = Rect.EMPTY
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
index d042d6d72f50..d3001d8fdcaf 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
@@ -66,16 +66,6 @@ class OpenTrampolineActivityTest(flicker: LegacyFlickerTest) : ActivityEmbedding
}
}
- /** Assert the background animation layer is never visible during bounds change transition. */
- @Presubmit
- @Test
- fun backgroundLayerNeverVisible() {
- val backgroundColorLayer = ComponentNameMatcher("", "Animation Background")
- flicker.assertLayers {
- isInvisible(backgroundColorLayer)
- }
- }
-
/** Trampoline activity should finish itself before the end of this test. */
@Presubmit
@Test
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
index ade1491fa17b..883c7e6d5785 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
@@ -45,18 +45,22 @@ constructor(
* based on the split pair rule.
*/
fun launchSecondaryActivity(wmHelper: WindowManagerStateHelper) {
- val launchButton =
- uiDevice.wait(
- Until.findObject(By.res(getPackage(), "launch_secondary_activity_button")),
- FIND_TIMEOUT
- )
- require(launchButton != null) { "Can't find launch secondary activity button on screen." }
- launchButton.click()
- wmHelper
- .StateSyncBuilder()
- .withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
- .withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
- .waitForAndVerify()
+ launchSecondaryActivityFromButton(wmHelper, "launch_secondary_activity_button")
+ }
+
+ /**
+ * Clicks the button to launch the secondary activity in RTL, which should split with the main
+ * activity based on the split pair rule.
+ */
+ fun launchSecondaryActivityRTL(wmHelper: WindowManagerStateHelper) {
+ launchSecondaryActivityFromButton(wmHelper, "launch_secondary_activity_rtl_button")
+ }
+
+ /**
+ * Clicks the button to launch the secondary activity in a horizontal split.
+ */
+ fun launchSecondaryActivityHorizontally(wmHelper: WindowManagerStateHelper) {
+ launchSecondaryActivityFromButton(wmHelper, "launch_secondary_activity_horizontally_button")
}
/** Clicks the button to launch a third activity over a secondary activity. */
@@ -101,16 +105,38 @@ constructor(
*/
fun finishSecondaryActivity(wmHelper: WindowManagerStateHelper) {
val finishButton =
- uiDevice.wait(
- Until.findObject(By.res(getPackage(), "finish_secondary_activity_button")),
- FIND_TIMEOUT
- )
+ uiDevice.wait(
+ Until.findObject(By.res(getPackage(), "finish_secondary_activity_button")),
+ FIND_TIMEOUT
+ )
require(finishButton != null) { "Can't find finish secondary activity button on screen." }
finishButton.click()
wmHelper
- .StateSyncBuilder()
- .withActivityRemoved(SECONDARY_ACTIVITY_COMPONENT)
- .waitForAndVerify()
+ .StateSyncBuilder()
+ .withActivityRemoved(SECONDARY_ACTIVITY_COMPONENT)
+ .waitForAndVerify()
+ }
+
+ /**
+ * Clicks the button to toggle the split ratio of secondary activity.
+ */
+ fun changeSecondaryActivityRatio(wmHelper: WindowManagerStateHelper) {
+ val launchButton =
+ uiDevice.wait(
+ Until.findObject(
+ By.res(getPackage(),
+ "toggle_split_ratio_button")),
+ FIND_TIMEOUT
+ )
+ require(launchButton != null) {
+ "Can't find toggle ratio for secondary activity button on screen."
+ }
+ launchButton.click()
+ wmHelper
+ .StateSyncBuilder()
+ .withAppTransitionIdle()
+ .withTransitionSnapshotGone()
+ .waitForAndVerify()
}
fun secondaryActivityEnterPip(wmHelper: WindowManagerStateHelper) {
@@ -149,25 +175,19 @@ constructor(
.waitForAndVerify()
}
- /**
- * Clicks the button to launch the secondary activity in RTL, which should split with the main
- * activity based on the split pair rule.
- */
- fun launchSecondaryActivityRTL(wmHelper: WindowManagerStateHelper) {
+ private fun launchSecondaryActivityFromButton(
+ wmHelper: WindowManagerStateHelper, buttonName: String) {
val launchButton =
- uiDevice.wait(
- Until.findObject(By.res(getPackage(), "launch_secondary_activity_rtl_button")),
- FIND_TIMEOUT
- )
+ uiDevice.wait(Until.findObject(By.res(getPackage(), buttonName)), FIND_TIMEOUT)
require(launchButton != null) {
- "Can't find launch secondary activity rtl button on screen."
+ "Can't find launch secondary activity button : " + buttonName + "on screen."
}
launchButton.click()
wmHelper
- .StateSyncBuilder()
- .withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
- .withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
- .waitForAndVerify()
+ .StateSyncBuilder()
+ .withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
+ .withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
+ .waitForAndVerify()
}
/**
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml
index e32a7092bf5d..86c21906163f 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml
@@ -38,6 +38,14 @@
android:text="Launch Secondary Activity in RTL" />
<Button
+ android:id="@+id/launch_secondary_activity_horizontally_button"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:onClick="launchSecondaryActivity"
+ android:tag="BOTTOM_TO_TOP"
+ android:text="Launch Secondary Activity Horizontally" />
+
+ <Button
android:id="@+id/launch_placeholder_split_button"
android:layout_width="wrap_content"
android:layout_height="48dp"
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml
index 135140aa2377..6d4de995bd73 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml
@@ -35,6 +35,14 @@
android:onClick="launchThirdActivity"
android:text="Launch a third activity" />
+ <ToggleButton
+ android:id="@+id/toggle_split_ratio_button"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:textOn="Ratio 0.5"
+ android:textOff="Ratio 0.3"
+ android:checked="false" />
+
<Button
android:id="@+id/secondary_enter_pip_button"
android:layout_width="wrap_content"
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java
index 3b1a8599f3e1..23fa91c37728 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java
@@ -22,6 +22,7 @@ import android.content.Intent;
import android.os.Bundle;
import android.view.View;
+import androidx.annotation.NonNull;
import androidx.window.embedding.ActivityFilter;
import androidx.window.embedding.ActivityRule;
import androidx.window.embedding.EmbeddingAspectRatio;
@@ -152,6 +153,9 @@ public class ActivityEmbeddingMainActivity extends Activity {
if (layoutDirectionStr.equals(LayoutDirection.LEFT_TO_RIGHT.toString())) {
return LayoutDirection.LEFT_TO_RIGHT;
}
+ if (layoutDirectionStr.equals(LayoutDirection.BOTTOM_TO_TOP.toString())) {
+ return LayoutDirection.BOTTOM_TO_TOP;
+ }
if (layoutDirectionStr.equals(LayoutDirection.RIGHT_TO_LEFT.toString())) {
return LayoutDirection.RIGHT_TO_LEFT;
}
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
index ee087ef9be2c..29cbf01dc6da 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
@@ -22,6 +22,11 @@ import android.app.PictureInPictureParams;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
+import android.widget.ToggleButton;
+
+import androidx.window.embedding.SplitAttributes;
+import androidx.window.embedding.SplitAttributesCalculatorParams;
+import androidx.window.embedding.SplitController;
/**
* Activity to be used as the secondary activity to split with
@@ -29,18 +34,41 @@ import android.view.View;
*/
public class ActivityEmbeddingSecondaryActivity extends Activity {
+ private SplitController mSplitController;
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_embedding_secondary_activity_layout);
findViewById(R.id.secondary_activity_layout).setBackgroundColor(Color.YELLOW);
findViewById(R.id.finish_secondary_activity_button).setOnClickListener(
- new View.OnClickListener() {
+ new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
- });
+ });
+ mSplitController = SplitController.getInstance(this);
+ final ToggleButton splitRatio = findViewById(R.id.toggle_split_ratio_button);
+ mSplitController.setSplitAttributesCalculator(params -> {
+ return new SplitAttributes.Builder()
+ .setSplitType(
+ SplitAttributes.SplitType.ratio(
+ splitRatio.isChecked() ? 0.7f : 0.5f)
+ )
+ .setLayoutDirection(
+ params.getDefaultSplitAttributes()
+ .getLayoutDirection())
+ .build();
+ });
+ splitRatio.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // This triggers a recalcuation of splitatributes.
+ mSplitController.invalidateTopVisibleSplitAttributes();
+ }
+ });
findViewById(R.id.secondary_enter_pip_button).setOnClickListener(
new View.OnClickListener() {
@Override
diff --git a/tests/Input/src/com/android/test/input/MotionPredictorTest.kt b/tests/Input/src/com/android/test/input/MotionPredictorTest.kt
index 24a567130ff0..d3eeac147c2a 100644
--- a/tests/Input/src/com/android/test/input/MotionPredictorTest.kt
+++ b/tests/Input/src/com/android/test/input/MotionPredictorTest.kt
@@ -129,7 +129,7 @@ class MotionPredictorTest {
// Prediction will happen for t=12 (since it is the next input interval after the requested
// time, 8, plus the model offset, 1).
assertEquals(12, predicted!!.eventTime)
- assertEquals(30f, predicted.x, /*delta=*/5f)
+ assertEquals(30f, predicted.x, /*delta=*/10f)
assertEquals(60f, predicted.y, /*delta=*/15f)
}
}