Loading packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java +102 −29 Original line number Diff line number Diff line Loading @@ -18,25 +18,36 @@ package com.android.systemui.statusbar.policy; import static com.android.server.notification.Flags.screenshareNotificationHiding; import android.annotation.MainThread; import android.app.IActivityManager; import android.content.Context; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionManager; import android.os.Handler; import android.os.RemoteException; import android.os.Trace; import android.service.notification.StatusBarNotification; import android.util.ArraySet; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.util.Assert; import com.android.systemui.util.ListenerSet; import java.util.concurrent.Executor; import javax.inject.Inject; /** Implementation of SensitiveNotificationProtectionController. **/ @SysUISingleton public class SensitiveNotificationProtectionControllerImpl implements SensitiveNotificationProtectionController { private final MediaProjectionManager mMediaProjectionManager; private static final String LOG_TAG = "SNPC"; private final ArraySet<String> mExemptPackages = new ArraySet<>(); private final ListenerSet<Runnable> mListeners = new ListenerSet<>(); private volatile MediaProjectionInfo mProjection; Loading @@ -45,23 +56,91 @@ public class SensitiveNotificationProtectionControllerImpl new MediaProjectionManager.Callback() { @Override public void onStart(MediaProjectionInfo info) { Trace.beginSection( "SNPC.onProjectionStart"); Trace.beginSection("SNPC.onProjectionStart"); try { // Only enable sensitive content protection if sharing full screen // Launch cookie only set (non-null) if sharing single app/task updateProjectionState((info.getLaunchCookie() == null) ? info : null); updateProjectionStateAndNotifyListeners( (info.getLaunchCookie() == null) ? info : null); } finally { Trace.endSection(); } } @Override public void onStop(MediaProjectionInfo info) { Trace.beginSection( "SNPC.onProjectionStop"); updateProjectionState(null); Trace.beginSection("SNPC.onProjectionStop"); try { updateProjectionStateAndNotifyListeners(null); } finally { Trace.endSection(); } } }; @Inject public SensitiveNotificationProtectionControllerImpl( Context context, MediaProjectionManager mediaProjectionManager, IActivityManager activityManager, @Main Handler mainHandler, @Background Executor bgExecutor) { if (!screenshareNotificationHiding()) { return; } private void updateProjectionState(MediaProjectionInfo info) { bgExecutor.execute(() -> { ArraySet<String> exemptPackages = new ArraySet<>(); // Exempt SystemUI exemptPackages.add(context.getPackageName()); // Exempt approved bug report handlers try { exemptPackages.addAll(activityManager.getBugreportWhitelistedPackages()); } catch (RemoteException e) { Log.e( LOG_TAG, "Error adding bug report handlers to exemption, continuing without", e); // silent failure, skip adding packages to exemption } // if currently projecting, notify listeners of exemption changes mainHandler.post(() -> { Trace.beginSection("SNPC.exemptPackagesUpdated"); try { updateExemptPackagesAndNotifyListeners(exemptPackages); } finally { Trace.endSection(); } }); }); mediaProjectionManager.addCallback(mMediaProjectionCallback, mainHandler); } /** * Notify listeners of possible ProjectionState change regardless of current * isSensitiveStateActive value. Method used to ensure updates occur after mExemptPackages gets * updated, which directly changes the outcome of isSensitiveStateActive */ @MainThread private void updateExemptPackagesAndNotifyListeners(ArraySet<String> exemptPackages) { Assert.isMainThread(); mExemptPackages.addAll(exemptPackages); if (mProjection != null) { mListeners.forEach(Runnable::run); } } /** * Update ProjectionState respecting current isSensitiveStateActive value. Only notifies * listeners */ @MainThread private void updateProjectionStateAndNotifyListeners(MediaProjectionInfo info) { Assert.isMainThread(); // capture previous state boolean wasSensitive = isSensitiveStateActive(); Loading @@ -73,18 +152,6 @@ public class SensitiveNotificationProtectionControllerImpl mListeners.forEach(Runnable::run); } } }; @Inject public SensitiveNotificationProtectionControllerImpl( MediaProjectionManager mediaProjectionManager, @Main Handler mainHandler) { mMediaProjectionManager = mediaProjectionManager; if (screenshareNotificationHiding()) { mMediaProjectionManager.addCallback(mMediaProjectionCallback, mainHandler); } } @Override public void registerSensitiveStateListener(Runnable onSensitiveStateChanged) { Loading @@ -96,11 +163,17 @@ public class SensitiveNotificationProtectionControllerImpl mListeners.remove(onSensitiveStateChanged); } // TODO(b/323396693): opportunity for optimization @Override public boolean isSensitiveStateActive() { MediaProjectionInfo projection = mProjection; if (projection == null) { return false; } // TODO(b/316955558): Add disabled by developer option // TODO(b/316955306): Add feature exemption for sysui and bug handlers return mProjection != null; return !mExemptPackages.contains(projection.getPackageName()); } @Override Loading packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt 0 → 100644 +63 −0 Original line number Diff line number Diff line /* * 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.IActivityManager import android.media.projection.MediaProjectionManager import android.os.Handler import android.platform.test.annotations.DisableFlags import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.server.notification.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verifyZeroInteractions import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) @DisableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING) class SensitiveNotificationProtectionControllerFlagDisabledTest : SysuiTestCase() { @Mock private lateinit var handler: Handler @Mock private lateinit var activityManager: IActivityManager @Mock private lateinit var mediaProjectionManager: MediaProjectionManager private lateinit var controller: SensitiveNotificationProtectionControllerImpl @Before fun setUp() { MockitoAnnotations.initMocks(this) controller = SensitiveNotificationProtectionControllerImpl( mContext, mediaProjectionManager, activityManager, handler, FakeExecutor(FakeSystemClock()) ) } @Test fun init_noRegisterMediaProjectionManagerCallback() { verifyZeroInteractions(mediaProjectionManager) } } packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt +107 −61 Original line number Diff line number Diff line Loading @@ -17,88 +17,93 @@ package com.android.systemui.statusbar.policy import android.app.ActivityOptions import android.app.IActivityManager import android.app.Notification import android.media.projection.MediaProjectionInfo import android.media.projection.MediaProjectionManager import android.os.Handler import android.platform.test.annotations.EnableFlags import android.service.notification.StatusBarNotification import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.server.notification.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.concurrency.mockExecutorHandler import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.withArgCaptor import com.android.systemui.util.time.FakeSystemClock 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) @RunWithLooper @EnableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING) class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { @Mock private lateinit var handler: Handler @Mock private lateinit var activityManager: IActivityManager @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 mediaProjectionCallback: MediaProjectionManager.Callback private lateinit var controller: SensitiveNotificationProtectionControllerImpl @Before fun setUp() { allowTestableLooperAsMainThread() // for updating exempt packages and notifying listeners MockitoAnnotations.initMocks(this) mSetFlagsRule.enableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING) setShareFullScreen() whenever(activityManager.bugreportWhitelistedPackages) .thenReturn(listOf(BUGREPORT_PACKAGE_NAME)) controller = SensitiveNotificationProtectionControllerImpl(mediaProjectionManager, handler) val executor = FakeExecutor(FakeSystemClock()) controller = SensitiveNotificationProtectionControllerImpl( mContext, mediaProjectionManager, activityManager, mockExecutorHandler(executor), executor ) // Process exemption processing executor.runAllReady() // Obtain useful MediaProjectionCallback verify(mediaProjectionManager).addCallback(mediaProjectionCallbackCaptor.capture(), any()) mediaProjectionCallback = withArgCaptor { verify(mediaProjectionManager).addCallback(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) fun init_registerMediaProjectionManagerCallback() { assertNotNull(mediaProjectionCallback) } @Test fun registerSensitiveStateListener_singleListener() { controller.registerSensitiveStateListener(listener1) mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) mediaProjectionCallback.onStop(mediaProjectionInfo) verify(listener1, times(2)).run() } Loading @@ -108,8 +113,8 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { controller.registerSensitiveStateListener(listener1) controller.registerSensitiveStateListener(listener2) mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) mediaProjectionCallback.onStop(mediaProjectionInfo) verify(listener1, times(2)).run() verify(listener2, times(2)).run() Loading @@ -117,12 +122,12 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { @Test fun registerSensitiveStateListener_afterProjectionActive() { mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) controller.registerSensitiveStateListener(listener1) verifyZeroInteractions(listener1) mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) mediaProjectionCallback.onStop(mediaProjectionInfo) verify(listener1).run() } Loading @@ -131,15 +136,15 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { fun unregisterSensitiveStateListener_singleListener() { controller.registerSensitiveStateListener(listener1) mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) mediaProjectionCallback.onStop(mediaProjectionInfo) verify(listener1, times(2)).run() controller.unregisterSensitiveStateListener(listener1) mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) mediaProjectionCallback.onStop(mediaProjectionInfo) verifyNoMoreInteractions(listener1) } Loading @@ -150,8 +155,8 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { controller.registerSensitiveStateListener(listener2) controller.registerSensitiveStateListener(listener3) mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) mediaProjectionCallback.onStop(mediaProjectionInfo) verify(listener1, times(2)).run() verify(listener2, times(2)).run() Loading @@ -160,8 +165,8 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { controller.unregisterSensitiveStateListener(listener1) controller.unregisterSensitiveStateListener(listener2) mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) mediaProjectionCallback.onStop(mediaProjectionInfo) verifyNoMoreInteractions(listener1) verifyNoMoreInteractions(listener2) Loading @@ -175,24 +180,24 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { @Test fun isSensitiveStateActive_projectionActive_true() { mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) assertTrue(controller.isSensitiveStateActive) } @Test fun isSensitiveStateActive_projectionInactiveAfterActive_false() { mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) mediaProjectionCallback.onStop(mediaProjectionInfo) assertFalse(controller.isSensitiveStateActive) } @Test fun isSensitiveStateActive_projectionActiveAfterInactive_true() { mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) mediaProjectionCallback.onStop(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) assertTrue(controller.isSensitiveStateActive) } Loading @@ -200,7 +205,25 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { @Test fun isSensitiveStateActive_projectionActive_singleActivity_false() { setShareSingleApp() mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) assertFalse(controller.isSensitiveStateActive) } @Test fun isSensitiveStateActive_projectionActive_sysuiExempt_false() { // SystemUi context packge name is exempt, but in test scenarios its // com.android.systemui.tests so use that instead of hardcoding whenever(mediaProjectionInfo.packageName).thenReturn(mContext.packageName) mediaProjectionCallback.onStart(mediaProjectionInfo) assertFalse(controller.isSensitiveStateActive) } @Test fun isSensitiveStateActive_projectionActive_bugReportHandlerExempt_false() { whenever(mediaProjectionInfo.packageName).thenReturn(BUGREPORT_PACKAGE_NAME) mediaProjectionCallback.onStart(mediaProjectionInfo) assertFalse(controller.isSensitiveStateActive) } Loading @@ -215,7 +238,7 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { @Test fun shouldProtectNotification_projectionActive_singleActivity_false() { setShareSingleApp() mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME) Loading @@ -224,7 +247,7 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { @Test fun shouldProtectNotification_projectionActive_fgsNotificationFromProjectionApp_false() { mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) val notificationEntry = setupFgsNotificationEntry(TEST_PROJECTION_PACKAGE_NAME) Loading @@ -233,7 +256,7 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { @Test fun shouldProtectNotification_projectionActive_fgsNotificationNotFromProjectionApp_true() { mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) val notificationEntry = setupFgsNotificationEntry(TEST_PACKAGE_NAME) Loading @@ -242,21 +265,43 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { @Test fun shouldProtectNotification_projectionActive_notFgsNotification_true() { mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) val notificationEntry = setupNotificationEntry(TEST_PROJECTION_PACKAGE_NAME) assertTrue(controller.shouldProtectNotification(notificationEntry)) } @Test fun shouldProtectNotification_projectionActive_sysuiExempt_false() { // SystemUi context packge name is exempt, but in test scenarios its // com.android.systemui.tests so use that instead of hardcoding whenever(mediaProjectionInfo.packageName).thenReturn(mContext.packageName) mediaProjectionCallback.onStart(mediaProjectionInfo) val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false) assertFalse(controller.shouldProtectNotification(notificationEntry)) } @Test fun shouldProtectNotification_projectionActive_bugReportHandlerExempt_false() { whenever(mediaProjectionInfo.packageName).thenReturn(BUGREPORT_PACKAGE_NAME) mediaProjectionCallback.onStart(mediaProjectionInfo) val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false) assertFalse(controller.shouldProtectNotification(notificationEntry)) } private fun setShareFullScreen() { `when`(mediaProjectionInfo.packageName).thenReturn(TEST_PROJECTION_PACKAGE_NAME) `when`(mediaProjectionInfo.launchCookie).thenReturn(null) whenever(mediaProjectionInfo.packageName).thenReturn(TEST_PROJECTION_PACKAGE_NAME) whenever(mediaProjectionInfo.launchCookie).thenReturn(null) } private fun setShareSingleApp() { `when`(mediaProjectionInfo.packageName).thenReturn(TEST_PROJECTION_PACKAGE_NAME) `when`(mediaProjectionInfo.launchCookie).thenReturn(ActivityOptions.LaunchCookie()) whenever(mediaProjectionInfo.packageName).thenReturn(TEST_PROJECTION_PACKAGE_NAME) whenever(mediaProjectionInfo.launchCookie).thenReturn(ActivityOptions.LaunchCookie()) } private fun setupNotificationEntry( Loading @@ -266,10 +311,10 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { 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.packageName).thenReturn(packageName) `when`(sbn.notification).thenReturn(notification) `when`(notification.isFgsOrUij).thenReturn(isFgs) whenever(notificationEntry.sbn).thenReturn(sbn) whenever(sbn.packageName).thenReturn(packageName) whenever(sbn.notification).thenReturn(notification) whenever(notification.isFgsOrUij).thenReturn(isFgs) return notificationEntry } Loading @@ -282,5 +327,6 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { private const val TEST_PROJECTION_PACKAGE_NAME = "com.android.systemui.statusbar.policy.projectionpackage" private const val TEST_PACKAGE_NAME = "com.android.systemui.statusbar.policy.testpackage" private const val BUGREPORT_PACKAGE_NAME = "com.android.test.bugreporthandler" } } Loading
packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java +102 −29 Original line number Diff line number Diff line Loading @@ -18,25 +18,36 @@ package com.android.systemui.statusbar.policy; import static com.android.server.notification.Flags.screenshareNotificationHiding; import android.annotation.MainThread; import android.app.IActivityManager; import android.content.Context; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionManager; import android.os.Handler; import android.os.RemoteException; import android.os.Trace; import android.service.notification.StatusBarNotification; import android.util.ArraySet; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.util.Assert; import com.android.systemui.util.ListenerSet; import java.util.concurrent.Executor; import javax.inject.Inject; /** Implementation of SensitiveNotificationProtectionController. **/ @SysUISingleton public class SensitiveNotificationProtectionControllerImpl implements SensitiveNotificationProtectionController { private final MediaProjectionManager mMediaProjectionManager; private static final String LOG_TAG = "SNPC"; private final ArraySet<String> mExemptPackages = new ArraySet<>(); private final ListenerSet<Runnable> mListeners = new ListenerSet<>(); private volatile MediaProjectionInfo mProjection; Loading @@ -45,23 +56,91 @@ public class SensitiveNotificationProtectionControllerImpl new MediaProjectionManager.Callback() { @Override public void onStart(MediaProjectionInfo info) { Trace.beginSection( "SNPC.onProjectionStart"); Trace.beginSection("SNPC.onProjectionStart"); try { // Only enable sensitive content protection if sharing full screen // Launch cookie only set (non-null) if sharing single app/task updateProjectionState((info.getLaunchCookie() == null) ? info : null); updateProjectionStateAndNotifyListeners( (info.getLaunchCookie() == null) ? info : null); } finally { Trace.endSection(); } } @Override public void onStop(MediaProjectionInfo info) { Trace.beginSection( "SNPC.onProjectionStop"); updateProjectionState(null); Trace.beginSection("SNPC.onProjectionStop"); try { updateProjectionStateAndNotifyListeners(null); } finally { Trace.endSection(); } } }; @Inject public SensitiveNotificationProtectionControllerImpl( Context context, MediaProjectionManager mediaProjectionManager, IActivityManager activityManager, @Main Handler mainHandler, @Background Executor bgExecutor) { if (!screenshareNotificationHiding()) { return; } private void updateProjectionState(MediaProjectionInfo info) { bgExecutor.execute(() -> { ArraySet<String> exemptPackages = new ArraySet<>(); // Exempt SystemUI exemptPackages.add(context.getPackageName()); // Exempt approved bug report handlers try { exemptPackages.addAll(activityManager.getBugreportWhitelistedPackages()); } catch (RemoteException e) { Log.e( LOG_TAG, "Error adding bug report handlers to exemption, continuing without", e); // silent failure, skip adding packages to exemption } // if currently projecting, notify listeners of exemption changes mainHandler.post(() -> { Trace.beginSection("SNPC.exemptPackagesUpdated"); try { updateExemptPackagesAndNotifyListeners(exemptPackages); } finally { Trace.endSection(); } }); }); mediaProjectionManager.addCallback(mMediaProjectionCallback, mainHandler); } /** * Notify listeners of possible ProjectionState change regardless of current * isSensitiveStateActive value. Method used to ensure updates occur after mExemptPackages gets * updated, which directly changes the outcome of isSensitiveStateActive */ @MainThread private void updateExemptPackagesAndNotifyListeners(ArraySet<String> exemptPackages) { Assert.isMainThread(); mExemptPackages.addAll(exemptPackages); if (mProjection != null) { mListeners.forEach(Runnable::run); } } /** * Update ProjectionState respecting current isSensitiveStateActive value. Only notifies * listeners */ @MainThread private void updateProjectionStateAndNotifyListeners(MediaProjectionInfo info) { Assert.isMainThread(); // capture previous state boolean wasSensitive = isSensitiveStateActive(); Loading @@ -73,18 +152,6 @@ public class SensitiveNotificationProtectionControllerImpl mListeners.forEach(Runnable::run); } } }; @Inject public SensitiveNotificationProtectionControllerImpl( MediaProjectionManager mediaProjectionManager, @Main Handler mainHandler) { mMediaProjectionManager = mediaProjectionManager; if (screenshareNotificationHiding()) { mMediaProjectionManager.addCallback(mMediaProjectionCallback, mainHandler); } } @Override public void registerSensitiveStateListener(Runnable onSensitiveStateChanged) { Loading @@ -96,11 +163,17 @@ public class SensitiveNotificationProtectionControllerImpl mListeners.remove(onSensitiveStateChanged); } // TODO(b/323396693): opportunity for optimization @Override public boolean isSensitiveStateActive() { MediaProjectionInfo projection = mProjection; if (projection == null) { return false; } // TODO(b/316955558): Add disabled by developer option // TODO(b/316955306): Add feature exemption for sysui and bug handlers return mProjection != null; return !mExemptPackages.contains(projection.getPackageName()); } @Override Loading
packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt 0 → 100644 +63 −0 Original line number Diff line number Diff line /* * 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.IActivityManager import android.media.projection.MediaProjectionManager import android.os.Handler import android.platform.test.annotations.DisableFlags import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.server.notification.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verifyZeroInteractions import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) @DisableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING) class SensitiveNotificationProtectionControllerFlagDisabledTest : SysuiTestCase() { @Mock private lateinit var handler: Handler @Mock private lateinit var activityManager: IActivityManager @Mock private lateinit var mediaProjectionManager: MediaProjectionManager private lateinit var controller: SensitiveNotificationProtectionControllerImpl @Before fun setUp() { MockitoAnnotations.initMocks(this) controller = SensitiveNotificationProtectionControllerImpl( mContext, mediaProjectionManager, activityManager, handler, FakeExecutor(FakeSystemClock()) ) } @Test fun init_noRegisterMediaProjectionManagerCallback() { verifyZeroInteractions(mediaProjectionManager) } }
packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt +107 −61 Original line number Diff line number Diff line Loading @@ -17,88 +17,93 @@ package com.android.systemui.statusbar.policy import android.app.ActivityOptions import android.app.IActivityManager import android.app.Notification import android.media.projection.MediaProjectionInfo import android.media.projection.MediaProjectionManager import android.os.Handler import android.platform.test.annotations.EnableFlags import android.service.notification.StatusBarNotification import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.server.notification.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.concurrency.mockExecutorHandler import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.withArgCaptor import com.android.systemui.util.time.FakeSystemClock 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) @RunWithLooper @EnableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING) class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { @Mock private lateinit var handler: Handler @Mock private lateinit var activityManager: IActivityManager @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 mediaProjectionCallback: MediaProjectionManager.Callback private lateinit var controller: SensitiveNotificationProtectionControllerImpl @Before fun setUp() { allowTestableLooperAsMainThread() // for updating exempt packages and notifying listeners MockitoAnnotations.initMocks(this) mSetFlagsRule.enableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING) setShareFullScreen() whenever(activityManager.bugreportWhitelistedPackages) .thenReturn(listOf(BUGREPORT_PACKAGE_NAME)) controller = SensitiveNotificationProtectionControllerImpl(mediaProjectionManager, handler) val executor = FakeExecutor(FakeSystemClock()) controller = SensitiveNotificationProtectionControllerImpl( mContext, mediaProjectionManager, activityManager, mockExecutorHandler(executor), executor ) // Process exemption processing executor.runAllReady() // Obtain useful MediaProjectionCallback verify(mediaProjectionManager).addCallback(mediaProjectionCallbackCaptor.capture(), any()) mediaProjectionCallback = withArgCaptor { verify(mediaProjectionManager).addCallback(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) fun init_registerMediaProjectionManagerCallback() { assertNotNull(mediaProjectionCallback) } @Test fun registerSensitiveStateListener_singleListener() { controller.registerSensitiveStateListener(listener1) mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) mediaProjectionCallback.onStop(mediaProjectionInfo) verify(listener1, times(2)).run() } Loading @@ -108,8 +113,8 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { controller.registerSensitiveStateListener(listener1) controller.registerSensitiveStateListener(listener2) mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) mediaProjectionCallback.onStop(mediaProjectionInfo) verify(listener1, times(2)).run() verify(listener2, times(2)).run() Loading @@ -117,12 +122,12 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { @Test fun registerSensitiveStateListener_afterProjectionActive() { mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) controller.registerSensitiveStateListener(listener1) verifyZeroInteractions(listener1) mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) mediaProjectionCallback.onStop(mediaProjectionInfo) verify(listener1).run() } Loading @@ -131,15 +136,15 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { fun unregisterSensitiveStateListener_singleListener() { controller.registerSensitiveStateListener(listener1) mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) mediaProjectionCallback.onStop(mediaProjectionInfo) verify(listener1, times(2)).run() controller.unregisterSensitiveStateListener(listener1) mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) mediaProjectionCallback.onStop(mediaProjectionInfo) verifyNoMoreInteractions(listener1) } Loading @@ -150,8 +155,8 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { controller.registerSensitiveStateListener(listener2) controller.registerSensitiveStateListener(listener3) mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) mediaProjectionCallback.onStop(mediaProjectionInfo) verify(listener1, times(2)).run() verify(listener2, times(2)).run() Loading @@ -160,8 +165,8 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { controller.unregisterSensitiveStateListener(listener1) controller.unregisterSensitiveStateListener(listener2) mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) mediaProjectionCallback.onStop(mediaProjectionInfo) verifyNoMoreInteractions(listener1) verifyNoMoreInteractions(listener2) Loading @@ -175,24 +180,24 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { @Test fun isSensitiveStateActive_projectionActive_true() { mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) assertTrue(controller.isSensitiveStateActive) } @Test fun isSensitiveStateActive_projectionInactiveAfterActive_false() { mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) mediaProjectionCallback.onStop(mediaProjectionInfo) assertFalse(controller.isSensitiveStateActive) } @Test fun isSensitiveStateActive_projectionActiveAfterInactive_true() { mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) mediaProjectionCallback.onStop(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) assertTrue(controller.isSensitiveStateActive) } Loading @@ -200,7 +205,25 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { @Test fun isSensitiveStateActive_projectionActive_singleActivity_false() { setShareSingleApp() mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) assertFalse(controller.isSensitiveStateActive) } @Test fun isSensitiveStateActive_projectionActive_sysuiExempt_false() { // SystemUi context packge name is exempt, but in test scenarios its // com.android.systemui.tests so use that instead of hardcoding whenever(mediaProjectionInfo.packageName).thenReturn(mContext.packageName) mediaProjectionCallback.onStart(mediaProjectionInfo) assertFalse(controller.isSensitiveStateActive) } @Test fun isSensitiveStateActive_projectionActive_bugReportHandlerExempt_false() { whenever(mediaProjectionInfo.packageName).thenReturn(BUGREPORT_PACKAGE_NAME) mediaProjectionCallback.onStart(mediaProjectionInfo) assertFalse(controller.isSensitiveStateActive) } Loading @@ -215,7 +238,7 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { @Test fun shouldProtectNotification_projectionActive_singleActivity_false() { setShareSingleApp() mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME) Loading @@ -224,7 +247,7 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { @Test fun shouldProtectNotification_projectionActive_fgsNotificationFromProjectionApp_false() { mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) val notificationEntry = setupFgsNotificationEntry(TEST_PROJECTION_PACKAGE_NAME) Loading @@ -233,7 +256,7 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { @Test fun shouldProtectNotification_projectionActive_fgsNotificationNotFromProjectionApp_true() { mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) val notificationEntry = setupFgsNotificationEntry(TEST_PACKAGE_NAME) Loading @@ -242,21 +265,43 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { @Test fun shouldProtectNotification_projectionActive_notFgsNotification_true() { mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) mediaProjectionCallback.onStart(mediaProjectionInfo) val notificationEntry = setupNotificationEntry(TEST_PROJECTION_PACKAGE_NAME) assertTrue(controller.shouldProtectNotification(notificationEntry)) } @Test fun shouldProtectNotification_projectionActive_sysuiExempt_false() { // SystemUi context packge name is exempt, but in test scenarios its // com.android.systemui.tests so use that instead of hardcoding whenever(mediaProjectionInfo.packageName).thenReturn(mContext.packageName) mediaProjectionCallback.onStart(mediaProjectionInfo) val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false) assertFalse(controller.shouldProtectNotification(notificationEntry)) } @Test fun shouldProtectNotification_projectionActive_bugReportHandlerExempt_false() { whenever(mediaProjectionInfo.packageName).thenReturn(BUGREPORT_PACKAGE_NAME) mediaProjectionCallback.onStart(mediaProjectionInfo) val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false) assertFalse(controller.shouldProtectNotification(notificationEntry)) } private fun setShareFullScreen() { `when`(mediaProjectionInfo.packageName).thenReturn(TEST_PROJECTION_PACKAGE_NAME) `when`(mediaProjectionInfo.launchCookie).thenReturn(null) whenever(mediaProjectionInfo.packageName).thenReturn(TEST_PROJECTION_PACKAGE_NAME) whenever(mediaProjectionInfo.launchCookie).thenReturn(null) } private fun setShareSingleApp() { `when`(mediaProjectionInfo.packageName).thenReturn(TEST_PROJECTION_PACKAGE_NAME) `when`(mediaProjectionInfo.launchCookie).thenReturn(ActivityOptions.LaunchCookie()) whenever(mediaProjectionInfo.packageName).thenReturn(TEST_PROJECTION_PACKAGE_NAME) whenever(mediaProjectionInfo.launchCookie).thenReturn(ActivityOptions.LaunchCookie()) } private fun setupNotificationEntry( Loading @@ -266,10 +311,10 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { 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.packageName).thenReturn(packageName) `when`(sbn.notification).thenReturn(notification) `when`(notification.isFgsOrUij).thenReturn(isFgs) whenever(notificationEntry.sbn).thenReturn(sbn) whenever(sbn.packageName).thenReturn(packageName) whenever(sbn.notification).thenReturn(notification) whenever(notification.isFgsOrUij).thenReturn(isFgs) return notificationEntry } Loading @@ -282,5 +327,6 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { private const val TEST_PROJECTION_PACKAGE_NAME = "com.android.systemui.statusbar.policy.projectionpackage" private const val TEST_PACKAGE_NAME = "com.android.systemui.statusbar.policy.testpackage" private const val BUGREPORT_PACKAGE_NAME = "com.android.test.bugreporthandler" } }