Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 0514919a authored by Richard MacGregor's avatar Richard MacGregor Committed by Android (Google) Code Review
Browse files

Merge "Add sensitive notification protection exemptions" into main

parents 18347dfd ceb43295
Loading
Loading
Loading
Loading
+102 −29
Original line number Diff line number Diff line
@@ -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;

@@ -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();

@@ -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) {
@@ -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
+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)
    }
}
+107 −61
Original line number Diff line number Diff line
@@ -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()
    }
@@ -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()
@@ -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()
    }
@@ -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)
    }
@@ -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()
@@ -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)
@@ -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)
    }
@@ -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)
    }
@@ -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)

@@ -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)

@@ -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)

@@ -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(
@@ -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
    }
@@ -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"
    }
}