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

Commit c3aa8fee authored by Patrick Williams's avatar Patrick Williams
Browse files

Add screen recording detection public APIs

Bug: 304574518
Flag: ACONFIG window_surfaces.screen_recording_callbacks DISABLED
Test: ScreenRecordingCallbackTests
Change-Id: I76ae2035d65989f17b620b16ae457e8fdd65cb9e
parent 47368cb3
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -90,6 +90,7 @@ package android {
    field public static final String DELETE_PACKAGES = "android.permission.DELETE_PACKAGES";
    field public static final String DELIVER_COMPANION_MESSAGES = "android.permission.DELIVER_COMPANION_MESSAGES";
    field public static final String DETECT_SCREEN_CAPTURE = "android.permission.DETECT_SCREEN_CAPTURE";
    field @FlaggedApi("com.android.window.flags.screen_recording_callbacks") public static final String DETECT_SCREEN_RECORDING = "android.permission.DETECT_SCREEN_RECORDING";
    field public static final String DIAGNOSTIC = "android.permission.DIAGNOSTIC";
    field public static final String DISABLE_KEYGUARD = "android.permission.DISABLE_KEYGUARD";
    field public static final String DUMP = "android.permission.DUMP";
@@ -53784,6 +53785,7 @@ package android.view {
    method public default void addCrossWindowBlurEnabledListener(@NonNull java.util.function.Consumer<java.lang.Boolean>);
    method public default void addCrossWindowBlurEnabledListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>);
    method public default void addProposedRotationListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer);
    method @FlaggedApi("com.android.window.flags.screen_recording_callbacks") @RequiresPermission(android.Manifest.permission.DETECT_SCREEN_RECORDING) public default int addScreenRecordingCallback(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
    method @NonNull public default android.view.WindowMetrics getCurrentWindowMetrics();
    method @Deprecated public android.view.Display getDefaultDisplay();
    method @NonNull public default android.view.WindowMetrics getMaximumWindowMetrics();
@@ -53793,6 +53795,7 @@ package android.view {
    method @FlaggedApi("com.android.window.flags.surface_control_input_receiver") public default void registerUnbatchedSurfaceControlInputReceiver(int, @NonNull android.os.IBinder, @NonNull android.view.SurfaceControl, @NonNull android.os.Looper, @NonNull android.view.SurfaceControlInputReceiver);
    method public default void removeCrossWindowBlurEnabledListener(@NonNull java.util.function.Consumer<java.lang.Boolean>);
    method public default void removeProposedRotationListener(@NonNull java.util.function.IntConsumer);
    method @FlaggedApi("com.android.window.flags.screen_recording_callbacks") @RequiresPermission(android.Manifest.permission.DETECT_SCREEN_RECORDING) public default void removeScreenRecordingCallback(@NonNull java.util.function.Consumer<java.lang.Integer>);
    method public void removeViewImmediate(android.view.View);
    method @FlaggedApi("com.android.window.flags.surface_control_input_receiver") public default void unregisterSurfaceControlInputReceiver(@NonNull android.view.SurfaceControl);
    method @FlaggedApi("com.android.window.flags.trusted_presentation_listener_for_window") public default void unregisterTrustedPresentationListener(@NonNull java.util.function.Consumer<java.lang.Boolean>);
@@ -53812,6 +53815,8 @@ package android.view {
    field public static final String PROPERTY_COMPAT_ENABLE_FAKE_FOCUS = "android.window.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS";
    field public static final String PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION = "android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION";
    field @FlaggedApi("com.android.window.flags.supports_multi_instance_system_ui") public static final String PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI = "android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI";
    field @FlaggedApi("com.android.window.flags.screen_recording_callbacks") public static final int SCREEN_RECORDING_STATE_NOT_VISIBLE = 0; // 0x0
    field @FlaggedApi("com.android.window.flags.screen_recording_callbacks") public static final int SCREEN_RECORDING_STATE_VISIBLE = 1; // 0x1
  }
  public static class WindowManager.BadTokenException extends java.lang.RuntimeException {
+2 −0
Original line number Diff line number Diff line
@@ -1085,7 +1085,9 @@ interface IWindowManager

    void unregisterTrustedPresentationListener(in ITrustedPresentationListener listener, int id);

    @EnforcePermission("DETECT_SCREEN_RECORDING")
    boolean registerScreenRecordingCallback(IScreenRecordingCallback callback);

    @EnforcePermission("DETECT_SCREEN_RECORDING")
    void unregisterScreenRecordingCallback(IScreenRecordingCallback callback);
}
+146 −0
Original line number Diff line number Diff line
/*
 * Copyright 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 android.view;

import static android.Manifest.permission.DETECT_SCREEN_RECORDING;
import static android.view.WindowManager.SCREEN_RECORDING_STATE_NOT_VISIBLE;
import static android.view.WindowManager.SCREEN_RECORDING_STATE_VISIBLE;

import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.os.Binder;
import android.os.RemoteException;
import android.util.ArrayMap;
import android.view.WindowManager.ScreenRecordingState;
import android.window.IScreenRecordingCallback;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
 * This class is responsible for calling app-registered screen recording callbacks. This class
 * registers a single screen recording callback with WindowManagerService and calls the
 * app-registered callbacks whenever that WindowManagerService callback is called.
 *
 * @hide
 */
public final class ScreenRecordingCallbacks {

    private static ScreenRecordingCallbacks sInstance;
    private static final Object sLock = new Object();

    private final ArrayMap<Consumer<@ScreenRecordingState Integer>, Executor> mCallbacks =
            new ArrayMap<>();

    private IScreenRecordingCallback mCallbackNotifier;
    private @ScreenRecordingState int mState = SCREEN_RECORDING_STATE_NOT_VISIBLE;

    private ScreenRecordingCallbacks() {}

    private static @NonNull IWindowManager getWindowManagerService() {
        return Objects.requireNonNull(WindowManagerGlobal.getWindowManagerService());
    }

    static ScreenRecordingCallbacks getInstance() {
        synchronized (sLock) {
            if (sInstance == null) {
                sInstance = new ScreenRecordingCallbacks();
            }
            return sInstance;
        }
    }

    @RequiresPermission(DETECT_SCREEN_RECORDING)
    @ScreenRecordingState
    int addCallback(
            @NonNull @CallbackExecutor Executor executor,
            @NonNull Consumer<@ScreenRecordingState Integer> callback) {
        synchronized (sLock) {
            if (mCallbackNotifier == null) {
                mCallbackNotifier =
                        new IScreenRecordingCallback.Stub() {
                            @Override
                            public void onScreenRecordingStateChanged(
                                    boolean visibleInScreenRecording) {
                                int state =
                                        visibleInScreenRecording
                                                ? SCREEN_RECORDING_STATE_VISIBLE
                                                : SCREEN_RECORDING_STATE_NOT_VISIBLE;
                                notifyCallbacks(state);
                            }
                        };
                try {
                    boolean visibleInScreenRecording =
                            getWindowManagerService()
                                    .registerScreenRecordingCallback(mCallbackNotifier);
                    mState =
                            visibleInScreenRecording
                                    ? SCREEN_RECORDING_STATE_VISIBLE
                                    : SCREEN_RECORDING_STATE_NOT_VISIBLE;
                } catch (RemoteException e) {
                    e.rethrowFromSystemServer();
                }
            }
            mCallbacks.put(callback, executor);
            return mState;
        }
    }

    @RequiresPermission(DETECT_SCREEN_RECORDING)
    void removeCallback(@NonNull Consumer<@ScreenRecordingState Integer> callback) {
        synchronized (sLock) {
            mCallbacks.remove(callback);
            if (mCallbacks.isEmpty()) {
                try {
                    getWindowManagerService().unregisterScreenRecordingCallback(mCallbackNotifier);
                } catch (RemoteException e) {
                    e.rethrowFromSystemServer();
                }
                mCallbackNotifier = null;
            }
        }
    }

    private void notifyCallbacks(@ScreenRecordingState int state) {
        List<Runnable> callbacks;
        synchronized (sLock) {
            mState = state;
            if (mCallbacks.isEmpty()) {
                return;
            }

            callbacks = new ArrayList<>();
            for (int i = 0; i < mCallbacks.size(); i++) {
                Consumer<Integer> callback = mCallbacks.keyAt(i);
                Executor executor = mCallbacks.valueAt(i);
                callbacks.add(() -> executor.execute(() -> callback.accept(state)));
            }
        }
        final long token = Binder.clearCallingIdentity();
        try {
            for (int i = 0; i < callbacks.size(); i++) {
                callbacks.get(i).run();
            }
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }
}
+63 −0
Original line number Diff line number Diff line
@@ -128,8 +128,10 @@ import android.window.TrustedPresentationThresholds;

import com.android.window.flags.Flags;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -6117,4 +6119,65 @@ public interface WindowManager extends ViewManager {
        throw new UnsupportedOperationException(
                "getDefaultToken is not implemented");
    }

    /** @hide */
    @Target(ElementType.TYPE_USE)
    @IntDef(
            prefix = {"SCREEN_RECORDING_STATE"},
            value = {SCREEN_RECORDING_STATE_NOT_VISIBLE, SCREEN_RECORDING_STATE_VISIBLE})
    @Retention(RetentionPolicy.SOURCE)
    @interface ScreenRecordingState {}

    /** Indicates the app that registered the callback is not visible in screen recording. */
    @FlaggedApi(com.android.window.flags.Flags.FLAG_SCREEN_RECORDING_CALLBACKS)
    int SCREEN_RECORDING_STATE_NOT_VISIBLE = 0;

    /** Indicates the app that registered the callback is visible in screen recording. */
    @FlaggedApi(com.android.window.flags.Flags.FLAG_SCREEN_RECORDING_CALLBACKS)
    int SCREEN_RECORDING_STATE_VISIBLE = 1;

    /**
     * Adds a screen recording callback. The callback will be invoked whenever the app becomes
     * visible in screen recording or was visible in screen recording and becomes invisible in
     * screen recording.
     *
     * <p>An app is considered visible in screen recording if any activities owned by the
     * registering process's UID are being recorded.
     *
     * <p>Example:
     *
     * <pre>
     * windowManager.addScreenRecordingCallback(state -> {
     *     // handle change in screen recording state
     * });
     * </pre>
     *
     * @param executor The executor on which callback method will be invoked.
     * @param callback The callback that will be invoked when screen recording visibility changes.
     * @return the current screen recording state.
     * @see #SCREEN_RECORDING_STATE_NOT_VISIBLE
     * @see #SCREEN_RECORDING_STATE_VISIBLE
     */
    @SuppressLint("AndroidFrameworkRequiresPermission")
    @RequiresPermission(permission.DETECT_SCREEN_RECORDING)
    @FlaggedApi(com.android.window.flags.Flags.FLAG_SCREEN_RECORDING_CALLBACKS)
    default @ScreenRecordingState int addScreenRecordingCallback(
            @NonNull @CallbackExecutor Executor executor,
            @NonNull Consumer<@ScreenRecordingState Integer> callback) {
        throw new UnsupportedOperationException();
    }

    /**
     * Removes a screen recording callback.
     *
     * @param callback The callback to remove.
     * @see #addScreenRecordingCallback(Executor, Consumer)
     */
    @SuppressLint("AndroidFrameworkRequiresPermission")
    @RequiresPermission(permission.DETECT_SCREEN_RECORDING)
    @FlaggedApi(com.android.window.flags.Flags.FLAG_SCREEN_RECORDING_CALLBACKS)
    default void removeScreenRecordingCallback(
            @NonNull Consumer<@ScreenRecordingState Integer> callback) {
        throw new UnsupportedOperationException();
    }
}
+23 −0
Original line number Diff line number Diff line
@@ -20,6 +20,8 @@ import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW;
import static android.window.WindowProviderService.isWindowProviderService;

import static com.android.window.flags.Flags.screenRecordingCallbacks;

import android.annotation.CallbackExecutor;
import android.annotation.IntRange;
import android.annotation.NonNull;
@@ -551,4 +553,25 @@ public final class WindowManagerImpl implements WindowManager {
    public IBinder getSurfaceControlInputClientToken(@NonNull SurfaceControl surfaceControl) {
        return mGlobal.getSurfaceControlInputClientToken(surfaceControl);
    }

    @Override
    public @ScreenRecordingState int addScreenRecordingCallback(
            @NonNull @CallbackExecutor Executor executor,
            @NonNull Consumer<@ScreenRecordingState Integer> callback) {
        if (screenRecordingCallbacks()) {
            Objects.requireNonNull(executor, "executor must not be null");
            Objects.requireNonNull(callback, "callback must not be null");
            return ScreenRecordingCallbacks.getInstance().addCallback(executor, callback);
        }
        return SCREEN_RECORDING_STATE_NOT_VISIBLE;
    }

    @Override
    public void removeScreenRecordingCallback(
            @NonNull Consumer<@ScreenRecordingState Integer> callback) {
        if (screenRecordingCallbacks()) {
            Objects.requireNonNull(callback, "callback must not be null");
            ScreenRecordingCallbacks.getInstance().removeCallback(callback);
        }
    }
}
Loading