diff options
8 files changed, 1136 insertions, 57 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt index 380cdadd1361..ae4ba27775b8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.os.UserHandle import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.Flags.screenshareNotificationHiding import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.StatusBarState @@ -30,6 +31,7 @@ import com.android.systemui.statusbar.notification.collection.coordinator.dagger import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController import com.android.systemui.user.domain.interactor.SelectedUserInteractor import dagger.Binds import dagger.Module @@ -55,6 +57,8 @@ class SensitiveContentCoordinatorImpl @Inject constructor( private val statusBarStateController: StatusBarStateController, private val keyguardStateController: KeyguardStateController, private val selectedUserInteractor: SelectedUserInteractor, + private val sensitiveNotificationProtectionController: + SensitiveNotificationProtectionController, ) : Invalidator("SensitiveContentInvalidator"), SensitiveContentCoordinator, DynamicPrivacyController.Listener, @@ -82,10 +86,13 @@ class SensitiveContentCoordinatorImpl @Inject constructor( return } + val isSensitiveContentProtectionActive = screenshareNotificationHiding() && + sensitiveNotificationProtectionController.isSensitiveStateActive val currentUserId = lockscreenUserManager.currentUserId val devicePublic = lockscreenUserManager.isLockscreenPublicMode(currentUserId) - val deviceSensitive = devicePublic && - !lockscreenUserManager.userAllowsPrivateNotificationsInPublic(currentUserId) + val deviceSensitive = (devicePublic && + !lockscreenUserManager.userAllowsPrivateNotificationsInPublic(currentUserId)) || + isSensitiveContentProtectionActive val dynamicallyUnlocked = dynamicPrivacyController.isDynamicallyUnlocked for (entry in extractAllRepresentativeEntries(entries).filter { it.rowExists() }) { val notifUserId = entry.sbn.user.identifier @@ -105,9 +112,13 @@ class SensitiveContentCoordinatorImpl @Inject constructor( else -> lockscreenUserManager.needsSeparateWorkChallenge(notifUserId) } } + + val shouldProtectNotification = screenshareNotificationHiding() && + sensitiveNotificationProtectionController.shouldProtectNotification(entry) + val needsRedaction = lockscreenUserManager.needsRedaction(entry) val isSensitive = userPublic && needsRedaction - entry.setSensitive(isSensitive, deviceSensitive) + entry.setSensitive(isSensitive || shouldProtectNotification, deviceSensitive) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 6a66bb74f16d..9a8cc0ae33cb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -22,6 +22,7 @@ import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_N import static com.android.app.animation.Interpolators.STANDARD; import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING; import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME; +import static com.android.systemui.Flags.screenshareNotificationHiding; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnEmptySpaceClickListener; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnOverscrollTopChangedListener; @@ -135,6 +136,7 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController; import com.android.systemui.statusbar.policy.SplitShadeStateController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.tuner.TunerService; @@ -218,6 +220,8 @@ public class NotificationStackScrollLayoutController implements Dumpable { private final SecureSettings mSecureSettings; private final NotificationDismissibilityProvider mDismissibilityProvider; private final ActivityStarter mActivityStarter; + private final SensitiveNotificationProtectionController + mSensitiveNotificationProtectionController; private View mLongPressedView; @@ -295,6 +299,15 @@ public class NotificationStackScrollLayoutController implements Dumpable { } }; + private final Runnable mSensitiveStateChangedListener = new Runnable() { + @Override + public void run() { + // Animate false to protect against screen recording capturing content + // during the animation + updateSensitivenessWithAnimation(false); + } + }; + private final DynamicPrivacyController.Listener mDynamicPrivacyControllerListener = () -> { if (mView.isExpanded()) { // The bottom might change because we're using the final actual height of the view @@ -399,7 +412,20 @@ public class NotificationStackScrollLayoutController implements Dumpable { } private void updateSensitivenessWithAnimation(boolean animate) { - mView.updateSensitiveness(animate, mLockscreenUserManager.isAnyProfilePublicMode()); + Trace.beginSection("NSSLC.updateSensitivenessWithAnimation"); + if (screenshareNotificationHiding()) { + boolean isAnyProfilePublic = mLockscreenUserManager.isAnyProfilePublicMode(); + boolean isSensitiveContentProtectionActive = + mSensitiveNotificationProtectionController.isSensitiveStateActive(); + boolean isSensitive = isAnyProfilePublic || isSensitiveContentProtectionActive; + + // Only animate if in a non-sensitive state (not screen sharing) + boolean shouldAnimate = animate && !isSensitiveContentProtectionActive; + mView.updateSensitiveness(shouldAnimate, isSensitive); + } else { + mView.updateSensitiveness(animate, mLockscreenUserManager.isAnyProfilePublicMode()); + } + Trace.endSection(); } /** @@ -708,7 +734,8 @@ public class NotificationStackScrollLayoutController implements Dumpable { SecureSettings secureSettings, NotificationDismissibilityProvider dismissibilityProvider, ActivityStarter activityStarter, - SplitShadeStateController splitShadeStateController) { + SplitShadeStateController splitShadeStateController, + SensitiveNotificationProtectionController sensitiveNotificationProtectionController) { mView = view; mKeyguardTransitionRepo = keyguardTransitionRepo; mViewBinder = viewBinder; @@ -756,6 +783,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { mSecureSettings = secureSettings; mDismissibilityProvider = dismissibilityProvider; mActivityStarter = activityStarter; + mSensitiveNotificationProtectionController = sensitiveNotificationProtectionController; mView.passSplitShadeStateController(splitShadeStateController); mDumpManager.registerDumpable(this); updateResources(); @@ -860,6 +888,11 @@ public class NotificationStackScrollLayoutController implements Dumpable { mDeviceProvisionedController.addCallback(mDeviceProvisionedListener); mDeviceProvisionedListener.onDeviceProvisionedChanged(); + if (screenshareNotificationHiding()) { + mSensitiveNotificationProtectionController + .registerSensitiveStateListener(mSensitiveStateChangedListener); + } + if (mView.isAttachedToWindow()) { mOnAttachStateChangeListener.onViewAttachedToWindow(mView); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionController.java new file mode 100644 index 000000000000..970cc75bbb6b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionController.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy; + +import com.android.systemui.statusbar.notification.collection.NotificationEntry; + +/** + * A controller which provides the current sensitive notification protections status as well as + * to assist in feature usage and exemptions + */ +public interface SensitiveNotificationProtectionController { + /** + * Register a runnable that triggers on changes to protection state + * + * <p> onSensitiveStateChanged not invoked on registration + */ + void registerSensitiveStateListener(Runnable onSensitiveStateChanged); + + /** Unregister a previously registered onSensitiveStateChanged runnable */ + void unregisterSensitiveStateListener(Runnable onSensitiveStateChanged); + + /** Return {@code true} if device in state in which notifications should be protected */ + boolean isSensitiveStateActive(); + + /** Return {@code true} when notification should be protected */ + boolean shouldProtectNotification(NotificationEntry entry); +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java new file mode 100644 index 000000000000..3c4ca4465874 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy; + +import static com.android.systemui.Flags.screenshareNotificationHiding; + +import android.media.projection.MediaProjectionInfo; +import android.media.projection.MediaProjectionManager; +import android.os.Handler; +import android.os.Trace; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.util.ListenerSet; + +import javax.inject.Inject; + +/** Implementation of SensitiveNotificationProtectionController. **/ +@SysUISingleton +public class SensitiveNotificationProtectionControllerImpl + implements SensitiveNotificationProtectionController { + private final MediaProjectionManager mMediaProjectionManager; + private final ListenerSet<Runnable> mListeners = new ListenerSet<>(); + private volatile MediaProjectionInfo mProjection; + + @VisibleForTesting + final MediaProjectionManager.Callback mMediaProjectionCallback = + new MediaProjectionManager.Callback() { + @Override + public void onStart(MediaProjectionInfo info) { + Trace.beginSection( + "SNPC.onProjectionStart"); + mProjection = info; + mListeners.forEach(Runnable::run); + Trace.endSection(); + } + + @Override + public void onStop(MediaProjectionInfo info) { + Trace.beginSection( + "SNPC.onProjectionStop"); + mProjection = null; + mListeners.forEach(Runnable::run); + Trace.endSection(); + } + }; + + @Inject + public SensitiveNotificationProtectionControllerImpl( + MediaProjectionManager mediaProjectionManager, + @Main Handler mainHandler) { + mMediaProjectionManager = mediaProjectionManager; + + if (screenshareNotificationHiding()) { + mMediaProjectionManager.addCallback(mMediaProjectionCallback, mainHandler); + } + } + + @Override + public void registerSensitiveStateListener(Runnable onSensitiveStateChanged) { + mListeners.addIfAbsent(onSensitiveStateChanged); + } + + @Override + public void unregisterSensitiveStateListener(Runnable onSensitiveStateChanged) { + mListeners.remove(onSensitiveStateChanged); + } + + @Override + public boolean isSensitiveStateActive() { + // TODO(b/316955558): Add disabled by developer option + // TODO(b/316955306): Add feature exemption for sysui and bug handlers + // TODO(b/316955346): Add feature exemption for single app screen sharing + return mProjection != null; + } + + @Override + public boolean shouldProtectNotification(NotificationEntry entry) { + if (!isSensitiveStateActive()) { + return false; + } + + // Exempt foreground service notifications from protection in effort to keep screen share + // stop actions easily accessible + // TODO(b/316955208): Exempt FGS notifications only for app that started projection + return !entry.getSbn().getNotification().isFgsOrUij(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java index 3304b9827fd8..15200bd0ac54 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java @@ -60,6 +60,8 @@ import com.android.systemui.statusbar.policy.RotationLockController; import com.android.systemui.statusbar.policy.RotationLockControllerImpl; import com.android.systemui.statusbar.policy.SecurityController; import com.android.systemui.statusbar.policy.SecurityControllerImpl; +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController; +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionControllerImpl; import com.android.systemui.statusbar.policy.SplitShadeStateController; import com.android.systemui.statusbar.policy.SplitShadeStateControllerImpl; import com.android.systemui.statusbar.policy.UserInfoController; @@ -146,6 +148,11 @@ public interface StatusBarPolicyModule { /** */ @Binds + SensitiveNotificationProtectionController provideSensitiveNotificationProtectionController( + SensitiveNotificationProtectionControllerImpl controllerImpl); + + /** */ + @Binds UserInfoController provideUserInfoContrller(UserInfoControllerImpl controllerImpl); /** */ diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt index df547ae5883e..350ed2d9ff22 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt @@ -17,9 +17,11 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.os.UserHandle +import android.platform.test.annotations.EnableFlags import android.service.notification.StatusBarNotification import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING import com.android.systemui.SysuiTestCase import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.NotificationLockscreenUserManager @@ -33,6 +35,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.OnBefo import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq @@ -55,28 +58,31 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { val statusBarStateController: StatusBarStateController = mock() val keyguardStateController: KeyguardStateController = mock() val mSelectedUserInteractor: SelectedUserInteractor = mock() + val sensitiveNotificationProtectionController: SensitiveNotificationProtectionController = + mock() val coordinator: SensitiveContentCoordinator = - DaggerTestSensitiveContentCoordinatorComponent - .factory() - .create( - dynamicPrivacyController, - lockscreenUserManager, - keyguardUpdateMonitor, - statusBarStateController, - keyguardStateController, - mSelectedUserInteractor) - .coordinator + DaggerTestSensitiveContentCoordinatorComponent.factory() + .create( + dynamicPrivacyController, + lockscreenUserManager, + keyguardUpdateMonitor, + statusBarStateController, + keyguardStateController, + mSelectedUserInteractor, + sensitiveNotificationProtectionController + ) + .coordinator @Test fun onDynamicPrivacyChanged_invokeInvalidationListener() { coordinator.attach(pipeline) - val invalidator = withArgCaptor<Invalidator> { - verify(pipeline).addPreRenderInvalidator(capture()) - } - val dynamicPrivacyListener = withArgCaptor<DynamicPrivacyController.Listener> { - verify(dynamicPrivacyController).addListener(capture()) - } + val invalidator = + withArgCaptor<Invalidator> { verify(pipeline).addPreRenderInvalidator(capture()) } + val dynamicPrivacyListener = + withArgCaptor<DynamicPrivacyController.Listener> { + verify(dynamicPrivacyController).addListener(capture()) + } val invalidationListener = mock<Pluggable.PluggableListener<Invalidator>>() invalidator.setInvalidationListener(invalidationListener) @@ -89,9 +95,10 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { @Test fun onBeforeRenderList_deviceUnlocked_notifDoesNotNeedRedaction() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false) @@ -105,11 +112,59 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceUnlocked_notifDoesNotNeedRedaction_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, false) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(false, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceUnlocked_notifDoesNotNeedRedaction_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, false) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, false) + } + + @Test fun onBeforeRenderList_deviceUnlocked_notifWouldNeedRedaction() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false) @@ -123,11 +178,59 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceUnlocked_notifWouldNeedRedaction_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, true) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(false, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceUnlocked_notifWouldNeedRedaction_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, true) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, false) + } + + @Test fun onBeforeRenderList_deviceLocked_userAllowsPublicNotifs() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) @@ -141,17 +244,87 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceLocked_userAllowsPublicNotifs_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, false) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(false, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceLocked_userAllowsPublicNotifs_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, false) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, false) + } + + @Test fun onBeforeRenderList_deviceLocked_userDisallowsPublicNotifs_notifDoesNotNeedRedaction() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, false) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(false, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + @Suppress("ktlint:standard:max-line-length") + fun onBeforeRenderList_deviceLocked_userDisallowsPublicNotifs_notifDoesNotNeedRedaction_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) val entry = fakeNotification(1, false) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) @@ -159,17 +332,92 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + @Suppress("ktlint:standard:max-line-length") + fun onBeforeRenderList_deviceLocked_userDisallowsPublicNotifs_notifDoesNotNeedRedaction_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, false) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, true) + } + + @Test fun onBeforeRenderList_deviceLocked_notifNeedsRedaction() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceLocked_notifNeedsRedaction_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) val entry = fakeNotification(1, true) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceLocked_notifNeedsRedaction_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, true) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) @@ -179,15 +427,37 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { @Test fun onBeforeRenderList_deviceDynamicallyUnlocked_notifNeedsRedaction() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) + val entry = fakeNotification(1, true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(false, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceDynamicallyUnlocked_notifNeedsRedaction_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) val entry = fakeNotification(1, true) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) @@ -195,19 +465,96 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + @Suppress("ktlint:standard:max-line-length") + fun onBeforeRenderList_deviceDynamicallyUnlocked_notifNeedsRedaction_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) + val entry = fakeNotification(1, true) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, true) + } + + @Test fun onBeforeRenderList_deviceDynamicallyUnlocked_notifUserNeedsWorkChallenge() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) + whenever(lockscreenUserManager.needsSeparateWorkChallenge(2)).thenReturn(true) + val entry = fakeNotification(2, true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceDynamicallyUnlocked_notifUserNeedsWorkChallenge_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) whenever(lockscreenUserManager.needsSeparateWorkChallenge(2)).thenReturn(true) + val entry = fakeNotification(2, true) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, true) + } + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + @Suppress("ktlint:standard:max-line-length") + fun onBeforeRenderList_deviceDynamicallyUnlocked_notifUserNeedsWorkChallenge_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) + whenever(lockscreenUserManager.needsSeparateWorkChallenge(2)).thenReturn(true) val entry = fakeNotification(2, true) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) @@ -217,9 +564,10 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { @Test fun onBeforeRenderList_deviceDynamicallyUnlocked_deviceBiometricBypassingLockScreen() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) @@ -227,9 +575,11 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) whenever(statusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD) whenever(keyguardUpdateMonitor.getUserUnlockedWithBiometricAndIsBypassing(any())) - .thenReturn(true) - + .thenReturn(true) val entry = fakeNotification(2, true) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) + whenever(sensitiveNotificationProtectionController.shouldProtectNotification(any())) + .thenReturn(true) onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) @@ -237,15 +587,11 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { } private fun fakeNotification(notifUserId: Int, needsRedaction: Boolean): ListEntry { - val mockUserHandle = mock<UserHandle>().apply { - whenever(identifier).thenReturn(notifUserId) - } - val mockSbn: StatusBarNotification = mock<StatusBarNotification>().apply { - whenever(user).thenReturn(mockUserHandle) - } - val mockEntry = mock<NotificationEntry>().apply { - whenever(sbn).thenReturn(mockSbn) - } + val mockUserHandle = + mock<UserHandle>().apply { whenever(identifier).thenReturn(notifUserId) } + val mockSbn: StatusBarNotification = + mock<StatusBarNotification>().apply { whenever(user).thenReturn(mockUserHandle) } + val mockEntry = mock<NotificationEntry>().apply { whenever(sbn).thenReturn(mockSbn) } whenever(lockscreenUserManager.needsRedaction(mockEntry)).thenReturn(needsRedaction) whenever(mockEntry.rowExists()).thenReturn(true) return object : ListEntry("key", 0) { @@ -268,6 +614,8 @@ interface TestSensitiveContentCoordinatorComponent { @BindsInstance statusBarStateController: StatusBarStateController, @BindsInstance keyguardStateController: KeyguardStateController, @BindsInstance selectedUserInteractor: SelectedUserInteractor, + @BindsInstance + sensitiveNotificationProtectionController: SensitiveNotificationProtectionController, ): TestSensitiveContentCoordinatorComponent } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index 89f826b2049d..1ab4c32c7d08 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.stack; +import static com.android.systemui.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING; import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.StatusBarState.SHADE; @@ -32,6 +33,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static kotlinx.coroutines.flow.FlowKt.emptyFlow; @@ -39,6 +41,7 @@ import static kotlinx.coroutines.test.TestCoroutineDispatchersKt.StandardTestDis import android.metrics.LogMaker; import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.View; @@ -101,6 +104,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.tuner.TunerService; import com.android.systemui.util.settings.SecureSettings; @@ -172,10 +176,16 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { @Mock private ActivityStarter mActivityStarter; @Mock private KeyguardTransitionRepository mKeyguardTransitionRepo; @Mock private NotificationListViewBinder mViewBinder; + @Mock + private SensitiveNotificationProtectionController mSensitiveNotificationProtectionController; + + @Captor + private ArgumentCaptor<Runnable> mSensitiveStateListenerArgumentCaptor; @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor; + private final ActiveNotificationListRepository mActiveNotificationsRepository = new ActiveNotificationListRepository(); @@ -386,6 +396,23 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { } @Test + public void testOnUserChange_verifyNotSensitive() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + initController(/* viewIsAttached= */ true); + + ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor + .forClass(UserChangedListener.class); + + verify(mNotificationLockscreenUserManager) + .addUserChangedListener(userChangedCaptor.capture()); + reset(mNotificationStackScrollLayout); + + UserChangedListener changedListener = userChangedCaptor.getValue(); + changedListener.onUserChanged(0); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, false); + } + + @Test public void testOnUserChange_verifySensitiveProfile() { when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); initController(/* viewIsAttached= */ true); @@ -403,6 +430,80 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnUserChange_verifyNotSensitive_screenshareNotificationHidingEnabled() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + + ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor + .forClass(UserChangedListener.class); + + verify(mNotificationLockscreenUserManager) + .addUserChangedListener(userChangedCaptor.capture()); + reset(mNotificationStackScrollLayout); + + UserChangedListener changedListener = userChangedCaptor.getValue(); + changedListener.onUserChanged(0); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, false); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnUserChange_verifySensitiveProfile_screenshareNotificationHidingEnabled() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + + ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor + .forClass(UserChangedListener.class); + + verify(mNotificationLockscreenUserManager) + .addUserChangedListener(userChangedCaptor.capture()); + reset(mNotificationStackScrollLayout); + + UserChangedListener changedListener = userChangedCaptor.getValue(); + changedListener.onUserChanged(0); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnUserChange_verifySensitiveActive_screenshareNotificationHidingEnabled() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true); + initController(/* viewIsAttached= */ true); + + ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor + .forClass(UserChangedListener.class); + + verify(mNotificationLockscreenUserManager) + .addUserChangedListener(userChangedCaptor.capture()); + reset(mNotificationStackScrollLayout); + + UserChangedListener changedListener = userChangedCaptor.getValue(); + changedListener.onUserChanged(0); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test + public void testOnStatePostChange_verifyNotSensitive() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, false); + } + + @Test public void testOnStatePostChange_verifyIfProfileIsPublic() { when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); @@ -418,6 +519,194 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnStatePostChange_verifyNotSensitive_screenshareNotificationHidingEnabled() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, false); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnStatePostChange_verifyIfProfileIsPublic_screenshareNotificationHidingEnabled( + ) { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnStatePostChange_verifyIfSensitiveActive_screenshareNotificationHidingEnabled( + ) { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test + public void testOnStatePostChange_goingFullShade_verifyNotSensitive() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(true, false); + } + + @Test + public void testOnStatePostChange_goingFullShade_verifyIfProfileIsPublic() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); + when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(true, true); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnStatePostChange_goingFullShade_verifyNotSensitive_screenshareHideEnabled( + ) { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(true, false); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnStatePostChange_goingFullShade_verifyProfileIsPublic_screenshareHideEnabled( + ) { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); + when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(true, true); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnStatePostChange_goingFullShade_verifySensitiveActive_screenshareHideEnabled( + ) { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnProjectionStateChanged_verifyNotSensitive() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()) + .thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSensitiveNotificationProtectionController) + .registerSensitiveStateListener(mSensitiveStateListenerArgumentCaptor.capture()); + + mSensitiveStateListenerArgumentCaptor.getValue().run(); + + verify(mNotificationStackScrollLayout).updateSensitiveness(false, false); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnProjectionStateChanged_verifyIfProfileIsPublic() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSensitiveNotificationProtectionController) + .registerSensitiveStateListener(mSensitiveStateListenerArgumentCaptor.capture()); + + mSensitiveStateListenerArgumentCaptor.getValue().run(); + + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnProjectionStateChanged_verifyIfSensitiveActive() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true); + + initController(/* viewIsAttached= */ true); + verify(mSensitiveNotificationProtectionController) + .registerSensitiveStateListener(mSensitiveStateListenerArgumentCaptor.capture()); + + mSensitiveStateListenerArgumentCaptor.getValue().run(); + + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test public void testOnMenuShownLogging() { ExpandableNotificationRow row = mock(ExpandableNotificationRow.class, RETURNS_DEEP_STUBS); when(row.getEntry().getSbn().getLogMaker()).thenReturn(new LogMaker( @@ -666,6 +955,20 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { verify(mNotificationStackScrollLayout).updateEmptyShadeView(eq(false), anyBoolean()); } + @Test + @DisableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void sensitiveNotificationProtectionControllerListenerNotRegistered() { + initController(/* viewIsAttached= */ true); + verifyZeroInteractions(mSensitiveNotificationProtectionController); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void sensitiveNotificationProtectionControllerListenerRegistered() { + initController(/* viewIsAttached= */ true); + verify(mSensitiveNotificationProtectionController).registerSensitiveStateListener(any()); + } + private LogMaker logMatcher(int category, int type) { return argThat(new LogMatcher(category, type)); } @@ -744,7 +1047,8 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mSecureSettings, mock(NotificationDismissibilityProvider.class), mActivityStarter, - new ResourcesSplitShadeStateController()); + new ResourcesSplitShadeStateController(), + mSensitiveNotificationProtectionController); } static class LogMatcher implements ArgumentMatcher<LogMaker> { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt new file mode 100644 index 000000000000..cd5d5ed0d08e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy + +import android.app.Notification +import android.media.projection.MediaProjectionInfo +import android.media.projection.MediaProjectionManager +import android.os.Handler +import android.service.notification.StatusBarNotification +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.any +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.reset +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { + @Mock private lateinit var handler: Handler + + @Mock private lateinit var mediaProjectionManager: MediaProjectionManager + + @Mock private lateinit var mediaProjectionInfo: MediaProjectionInfo + + @Mock private lateinit var listener1: Runnable + @Mock private lateinit var listener2: Runnable + @Mock private lateinit var listener3: Runnable + + @Captor + private lateinit var mediaProjectionCallbackCaptor: + ArgumentCaptor<MediaProjectionManager.Callback> + + private lateinit var controller: SensitiveNotificationProtectionControllerImpl + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + mSetFlagsRule.enableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING) + + controller = SensitiveNotificationProtectionControllerImpl(mediaProjectionManager, handler) + + // Obtain useful MediaProjectionCallback + verify(mediaProjectionManager).addCallback(mediaProjectionCallbackCaptor.capture(), any()) + } + + @Test + fun init_flagEnabled_registerMediaProjectionManagerCallback() { + assertNotNull(mediaProjectionCallbackCaptor.value) + } + + @Test + fun init_flagDisabled_noRegisterMediaProjectionManagerCallback() { + mSetFlagsRule.disableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING) + reset(mediaProjectionManager) + + controller = SensitiveNotificationProtectionControllerImpl(mediaProjectionManager, handler) + + verifyZeroInteractions(mediaProjectionManager) + } + + @Test + fun registerSensitiveStateListener_singleListener() { + controller.registerSensitiveStateListener(listener1) + + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verify(listener1, times(2)).run() + } + + @Test + fun registerSensitiveStateListener_multipleListeners() { + controller.registerSensitiveStateListener(listener1) + controller.registerSensitiveStateListener(listener2) + + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verify(listener1, times(2)).run() + verify(listener2, times(2)).run() + } + + @Test + fun registerSensitiveStateListener_afterProjectionActive() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + controller.registerSensitiveStateListener(listener1) + verifyZeroInteractions(listener1) + + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verify(listener1).run() + } + + @Test + fun unregisterSensitiveStateListener_singleListener() { + controller.registerSensitiveStateListener(listener1) + + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verify(listener1, times(2)).run() + + controller.unregisterSensitiveStateListener(listener1) + + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verifyNoMoreInteractions(listener1) + } + + @Test + fun unregisterSensitiveStateListener_multipleListeners() { + controller.registerSensitiveStateListener(listener1) + controller.registerSensitiveStateListener(listener2) + controller.registerSensitiveStateListener(listener3) + + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verify(listener1, times(2)).run() + verify(listener2, times(2)).run() + verify(listener3, times(2)).run() + + controller.unregisterSensitiveStateListener(listener1) + controller.unregisterSensitiveStateListener(listener2) + + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verifyNoMoreInteractions(listener1) + verifyNoMoreInteractions(listener2) + verify(listener3, times(4)).run() + } + + @Test + fun isSensitiveStateActive_projectionInactive_false() { + assertFalse(controller.isSensitiveStateActive) + } + + @Test + fun isSensitiveStateActive_projectionActive_true() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + assertTrue(controller.isSensitiveStateActive) + } + + @Test + fun isSensitiveStateActive_projectionInactiveAfterActive_false() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + assertFalse(controller.isSensitiveStateActive) + } + + @Test + fun isSensitiveStateActive_projectionActiveAfterInactive_true() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + assertTrue(controller.isSensitiveStateActive) + } + + @Test + fun shouldProtectNotification_projectionInactive_false() { + val notificationEntry = mock(NotificationEntry::class.java) + + assertFalse(controller.shouldProtectNotification(notificationEntry)) + } + + @Test + fun shouldProtectNotification_projectionActive_fgsNotification_false() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + val notificationEntry = mock(NotificationEntry::class.java) + val sbn = mock(StatusBarNotification::class.java) + val notification = mock(Notification::class.java) + `when`(notificationEntry.sbn).thenReturn(sbn) + `when`(sbn.notification).thenReturn(notification) + `when`(notification.isFgsOrUij).thenReturn(true) + + assertFalse(controller.shouldProtectNotification(notificationEntry)) + } + + @Test + fun shouldProtectNotification_projectionActive_notFgsNotification_true() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + val notificationEntry = mock(NotificationEntry::class.java) + val sbn = mock(StatusBarNotification::class.java) + val notification = mock(Notification::class.java) + `when`(notificationEntry.sbn).thenReturn(sbn) + `when`(sbn.notification).thenReturn(notification) + `when`(notification.isFgsOrUij).thenReturn(false) + + assertTrue(controller.shouldProtectNotification(notificationEntry)) + } +} |