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

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

Merge "Sensitive notification content protection for apps" into main

parents 107adf7c 57c5e432
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