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

Commit 57c5e432 authored by Richard MacGregor's avatar Richard MacGregor
Browse files

Sensitive notification content protection for apps

Create component that monitors for:
- notifications with sensitive content
- media projection start/end

Apps will have secure window applied.

Flag: ACONFIG com.android.server.notification.sensitive_notification_app_protection DISABLED
Bug: 312785095
Test: atest SensitiveContentProtectionManagerServiceTest
Test: atest WindowStateTests
Test: atest WindowManagerServiceTests
Change-Id: I84a4d0dfaa26109ac82db0899f37c30912a6cc14
parent 337c2efd
Loading
Loading
Loading
Loading
+188 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server;

import static com.android.internal.util.Preconditions.checkNotNull;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.media.projection.MediaProjectionInfo;
import android.media.projection.MediaProjectionManager;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.os.UserHandle;
import android.service.notification.NotificationListenerService;
import android.service.notification.NotificationListenerService.RankingMap;
import android.service.notification.StatusBarNotification;
import android.util.ArraySet;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wm.SensitiveContentPackages.PackageInfo;
import com.android.server.wm.WindowManagerInternal;

import java.util.Collections;
import java.util.Set;

/**
 * Service that monitors for notifications with sensitive content and protects content from screen
 * sharing
 */
public final class SensitiveContentProtectionManagerService extends SystemService {
    private static final String TAG = "SensitiveContentProtect";
    private static final boolean DEBUG = false;

    @VisibleForTesting
    NotificationListener mNotificationListener;
    private @Nullable MediaProjectionManager mProjectionManager;
    private @Nullable WindowManagerInternal mWindowManager;

    private final MediaProjectionManager.Callback mProjectionCallback =
            new MediaProjectionManager.Callback() {
                @Override
                public void onStart(MediaProjectionInfo info) {
                    if (DEBUG) Log.d(TAG, "onStart projection: " + info);
                    onProjectionStart();
                }

                @Override
                public void onStop(MediaProjectionInfo info) {
                    if (DEBUG) Log.d(TAG, "onStop projection: " + info);
                    onProjectionEnd();
                }
            };

    public SensitiveContentProtectionManagerService(@NonNull Context context) {
        super(context);
        mNotificationListener = new NotificationListener();
    }

    @Override
    public void onStart() {}

    @Override
    public void onBootPhase(int phase) {
        if (phase != SystemService.PHASE_BOOT_COMPLETED) {
            return;
        }

        if (DEBUG) Log.d(TAG, "onBootPhase - PHASE_BOOT_COMPLETED");

        init(getContext().getSystemService(MediaProjectionManager.class),
                LocalServices.getService(WindowManagerInternal.class));
    }

    @VisibleForTesting
    void init(MediaProjectionManager projectionManager,
            WindowManagerInternal windowManager) {
        if (DEBUG) Log.d(TAG, "init");

        checkNotNull(projectionManager, "Failed to get valid MediaProjectionManager");
        checkNotNull(windowManager, "Failed to get valid WindowManagerInternal");

        mProjectionManager = projectionManager;
        mWindowManager = windowManager;

        // TODO(b/317250444): use MediaProjectionManagerService directly, reduces unnecessary
        //  handler, delegate, and binder death recipient
        mProjectionManager.addCallback(mProjectionCallback, new Handler(Looper.getMainLooper()));

        try {
            mNotificationListener.registerAsSystemService(getContext(),
                    new ComponentName(getContext(), NotificationListener.class),
                    UserHandle.USER_ALL);
        } catch (RemoteException e) {
            // Intra-process call, should never happen.
        }
    }

    /** Cleanup any callbacks and listeners */
    @VisibleForTesting
    void onDestroy() {
        if (mProjectionManager != null) {
            mProjectionManager.removeCallback(mProjectionCallback);
        }

        try {
            mNotificationListener.unregisterAsSystemService();
        } catch (RemoteException e) {
            // Intra-process call, should never happen.
        }

        if (mWindowManager != null) {
            onProjectionEnd();
        }
    }

    private void onProjectionStart() {
        StatusBarNotification[] notifications;
        try {
            notifications = mNotificationListener.getActiveNotifications();
        } catch (SecurityException e) {
            Log.e(TAG, "SensitiveContentProtectionManagerService doesn't have access.", e);
            notifications = new StatusBarNotification[0];
        }

        RankingMap rankingMap;
        try {
            rankingMap = mNotificationListener.getCurrentRanking();
        } catch (SecurityException e) {
            Log.e(TAG, "SensitiveContentProtectionManagerService doesn't have access.", e);
            rankingMap = null;
        }

        // notify windowmanager of any currently posted sensitive content notifications
        Set<PackageInfo> packageInfos = getSensitivePackagesFromNotifications(
                notifications,
                rankingMap);

        mWindowManager.setShouldBlockScreenCaptureForApp(packageInfos);
    }

    private void onProjectionEnd() {
        // notify windowmanager to clear any sensitive notifications observed during projection
        // session
        mWindowManager.setShouldBlockScreenCaptureForApp(Collections.emptySet());
    }

    private Set<PackageInfo> getSensitivePackagesFromNotifications(
            StatusBarNotification[] notifications, RankingMap rankingMap) {
        if (rankingMap == null) {
            Log.w(TAG, "Ranking map not initialized.");
            return Collections.emptySet();
        }

        Set<PackageInfo> sensitivePackages = new ArraySet<>();
        for (StatusBarNotification sbn : notifications) {
            NotificationListenerService.Ranking ranking =
                    rankingMap.getRawRankingObject(sbn.getKey());
            if (ranking != null && ranking.hasSensitiveContent()) {
                PackageInfo info = new PackageInfo(sbn.getPackageName(), sbn.getUid());
                sensitivePackages.add(info);
            }
        }
        return sensitivePackages;
    }

    // TODO(b/317251408): add trigger that updates on onNotificationPosted,
    //  onNotificationRankingUpdate and onListenerConnected
    @VisibleForTesting
    static class NotificationListener extends NotificationListenerService {}
}
+83 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.wm;

import android.annotation.NonNull;
import android.util.ArraySet;

import java.io.PrintWriter;
import java.util.Objects;
import java.util.Set;

/**
 * Cache of distinct package/uid pairs that require being blocked from screen capture. This class is
 * not threadsafe and any call site should hold {@link WindowManagerGlobalLock}
 */
public class SensitiveContentPackages {
    private final ArraySet<PackageInfo> mProtectedPackages = new ArraySet<>();

    /** Returns {@code true} if package/uid pair should be blocked from screen capture */
    public boolean shouldBlockScreenCaptureForApp(String pkg, int uid) {
        for (int i = 0; i < mProtectedPackages.size(); i++) {
            PackageInfo info = mProtectedPackages.valueAt(i);
            if (info != null && info.mPkg.equals(pkg) && info.mUid == uid) {
                return true;
            }
        }
        return false;
    }

    /** Replaces the set of package/uid pairs to set that should be blocked from screen capture */
    public void setShouldBlockScreenCaptureForApp(@NonNull Set<PackageInfo> packageInfos) {
        mProtectedPackages.clear();
        mProtectedPackages.addAll(packageInfos);
    }

    void dump(PrintWriter pw) {
        final String innerPrefix = "  ";
        pw.println("SensitiveContentPackages:");
        pw.println(innerPrefix + "Packages that should block screen capture ("
                + mProtectedPackages.size() + "):");
        for (PackageInfo info : mProtectedPackages) {
            pw.println(innerPrefix + "  package=" + info.mPkg + "  uid=" + info.mUid);
        }
    }

    /** Helper class that represents a package/uid pair */
    public static class PackageInfo {
        private String mPkg;
        private int mUid;

        public PackageInfo(String pkg, int uid) {
            this.mPkg = pkg;
            this.mUid = uid;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof PackageInfo)) return false;
            PackageInfo that = (PackageInfo) o;
            return mUid == that.mUid && Objects.equals(mPkg, that.mPkg);
        }

        @Override
        public int hashCode() {
            return Objects.hash(mPkg, mUid);
        }
    }
}
+9 −0
Original line number Diff line number Diff line
@@ -51,6 +51,7 @@ import android.window.ScreenCapture;
import com.android.internal.policy.KeyInterceptionInfo;
import com.android.server.input.InputManagerService;
import com.android.server.policy.WindowManagerPolicy;
import com.android.server.wm.SensitiveContentPackages.PackageInfo;

import java.lang.annotation.Retention;
import java.util.List;
@@ -1012,4 +1013,12 @@ public abstract class WindowManagerInternal {
     */
    public abstract void setOrientationRequestPolicy(boolean respected,
            int[] fromOrientations, int[] toOrientations);

    /**
     * Set whether screen capture should be disabled for all windows of a specific app windows based
     * on sensitive content protections.
     *
     * @param packageInfos set of {@link PackageInfo} whose windows should be blocked from capture
     */
    public abstract void setShouldBlockScreenCaptureForApp(@NonNull Set<PackageInfo> packageInfos);
}
+18 −2
Original line number Diff line number Diff line
@@ -123,6 +123,7 @@ import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_
import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
import static com.android.server.wm.RootWindowContainer.MATCH_ATTACHED_TASK_OR_RECENT_TASKS;
import static com.android.server.wm.SensitiveContentPackages.PackageInfo;
import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL;
import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
@@ -312,6 +313,7 @@ import android.window.WindowContainerToken;
import android.window.WindowContextInfo;

import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.IResultReceiver;
import com.android.internal.policy.IKeyguardDismissCallback;
@@ -366,6 +368,7 @@ import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@@ -1053,6 +1056,9 @@ public class WindowManagerService extends IWindowManager.Stub

    SystemPerformanceHinter mSystemPerformanceHinter;

    @GuardedBy("mGlobalLock")
    final SensitiveContentPackages mSensitiveContentPackages = new SensitiveContentPackages();

    /** Listener to notify activity manager about app transitions. */
    final WindowManagerInternal.AppTransitionListener mActivityManagerAppTransitionNotifier
            = new WindowManagerInternal.AppTransitionListener() {
@@ -1931,12 +1937,13 @@ public class WindowManagerService extends IWindowManager.Stub

    /**
     * Set whether screen capture is disabled for all windows of a specific user from
     * the device policy cache.
     * the device policy cache, or specific windows based on sensitive content protections.
     */
    @Override
    public void refreshScreenCaptureDisabled() {
        int callingUid = Binder.getCallingUid();
        if (callingUid != SYSTEM_UID) {
        // MY_UID (Process.myUid()) should always be SYSTEM_UID here, but using MY_UID for tests
        if (callingUid != MY_UID) {
            throw new SecurityException("Only system can call refreshScreenCaptureDisabled.");
        }

@@ -7169,6 +7176,7 @@ public class WindowManagerService extends IWindowManager.Stub
            }
            mSystemPerformanceHinter.dump(pw, "");
            mTrustedPresentationListenerController.dump(pw);
            mSensitiveContentPackages.dump(pw);
        }
    }

@@ -8550,6 +8558,14 @@ public class WindowManagerService extends IWindowManager.Stub
            InputTarget inputTarget = WindowManagerService.this.getInputTargetFromToken(inputToken);
            return inputTarget == null ? null : inputTarget.getWindowToken();
        }

        @Override
        public void setShouldBlockScreenCaptureForApp(Set<PackageInfo> packageInfos) {
            synchronized (mGlobalLock) {
                mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(packageInfos);
                WindowManagerService.this.refreshScreenCaptureDisabled();
            }
        }
    }

    private final class ImeTargetVisibilityPolicyImpl extends ImeTargetVisibilityPolicy {
+8 −0
Original line number Diff line number Diff line
@@ -1896,6 +1896,14 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
        if ((mAttrs.flags & WindowManager.LayoutParams.FLAG_SECURE) != 0) {
            return true;
        }

        if (com.android.server.notification.Flags.sensitiveNotificationAppProtection()) {
            if (mWmService.mSensitiveContentPackages
                    .shouldBlockScreenCaptureForApp(getOwningPackage(), getOwningUid())) {
                return true;
            }
        }

        return !DevicePolicyCache.getInstance().isScreenCaptureAllowed(mShowUserId);
    }

Loading