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

Commit ef47d1b9 authored by Matt Casey's avatar Matt Casey Committed by Automerger Merge Worker
Browse files

Merge "Migrate to ScreenshotData object" into tm-qpr-dev am: 6378bd57 am: 881ac485

parents fc16dc4b 881ac485
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -492,6 +492,9 @@ object Flags {
    val SCREENSHOT_WORK_PROFILE_POLICY =
        unreleasedFlag(1301, "screenshot_work_profile_policy", teamfood = true)

    // TODO(b/264916608): Tracking Bug
    @JvmField val SCREENSHOT_METADATA = unreleasedFlag(1302, "screenshot_metadata")

    // 1400 - columbus
    // TODO(b/254512756): Tracking Bug
    val QUICK_TAP_IN_PCC = releasedFlag(1400, "quick_tap_in_pcc")
+56 −0
Original line number Diff line number Diff line
@@ -45,6 +45,7 @@ class RequestProcessor @Inject constructor(
     *
     * @param request the request to process
     */
    // TODO: Delete once SCREENSHOT_METADATA flag is launched
    suspend fun process(request: ScreenshotRequest): ScreenshotRequest {
        var result = request

@@ -93,12 +94,67 @@ class RequestProcessor @Inject constructor(
     * @param request the request to process
     * @param callback the callback to provide the processed request, invoked from the main thread
     */
    // TODO: Delete once SCREENSHOT_METADATA flag is launched
    fun processAsync(request: ScreenshotRequest, callback: Consumer<ScreenshotRequest>) {
        mainScope.launch {
            val result = process(request)
            callback.accept(result)
        }
    }

    /**
     * Inspects the incoming ScreenshotData, potentially modifying it based upon policy.
     *
     * @param screenshot the screenshot to process
     */
    suspend fun process(screenshot: ScreenshotData): ScreenshotData {
        var result = screenshot

        // Apply work profile screenshots policy:
        //
        // If the focused app belongs to a work profile, transforms a full screen
        // (or partial) screenshot request to a task snapshot (provided image) screenshot.

        // Whenever displayContentInfo is fetched, the topComponent is also populated
        // regardless of the managed profile status.

        if (screenshot.type != TAKE_SCREENSHOT_PROVIDED_IMAGE &&
            flags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)
        ) {
            val info = policy.findPrimaryContent(policy.getDefaultDisplayId())
            Log.d(TAG, "findPrimaryContent: $info")
            result.taskId = info.taskId
            result.topComponent = info.component
            result.userHandle = info.user

            if (policy.isManagedProfile(info.user.identifier)) {
                val image = capture.captureTask(info.taskId)
                    ?: error("Task snapshot returned a null Bitmap!")

                // Provide the task snapshot as the screenshot
                result.type = TAKE_SCREENSHOT_PROVIDED_IMAGE
                result.bitmap = image
                result.screenBounds = info.bounds
            }
        }

        return result
    }

    /**
     * Note: This is for compatibility with existing Java. Prefer the suspending function when
     * calling from a Coroutine context.
     *
     * @param screenshot the screenshot to process
     * @param callback the callback to provide the processed screenshot, invoked from the main
     *                 thread
     */
    fun processAsync(screenshot: ScreenshotData, callback: Consumer<ScreenshotData>) {
        mainScope.launch {
            val result = process(screenshot)
            callback.accept(result)
        }
    }
}

private const val TAG = "RequestProcessor"
+162 −29
Original line number Diff line number Diff line
@@ -390,16 +390,131 @@ public class ScreenshotController {
                ClipboardOverlayController.SELF_PERMISSION, null, Context.RECEIVER_NOT_EXPORTED);
    }

    void handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher,
            RequestCallback requestCallback) {
        Assert.isMainThread();
        mCurrentRequestCallback = requestCallback;
        if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_FULLSCREEN) {
            Rect bounds = getFullScreenRect();
            screenshot.setBitmap(mImageCapture.captureDisplay(DEFAULT_DISPLAY, bounds));
            screenshot.setScreenBounds(bounds);
        }

        if (screenshot.getBitmap() == null) {
            Log.e(TAG, "handleScreenshot: Screenshot bitmap was null");
            mNotificationsController.notifyScreenshotError(
                    R.string.screenshot_failed_to_capture_text);
            if (mCurrentRequestCallback != null) {
                mCurrentRequestCallback.reportError();
            }
            return;
        }

        if (!isUserSetupComplete(Process.myUserHandle())) {
            Log.w(TAG, "User setup not complete, displaying toast only");
            // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing
            // and sharing shouldn't be exposed to the user.
            saveScreenshotAndToast(screenshot.getUserHandle(), finisher);
            return;
        }

        mBroadcastSender.sendBroadcast(new Intent(ClipboardOverlayController.SCREENSHOT_ACTION),
                ClipboardOverlayController.SELF_PERMISSION);

        mScreenshotTakenInPortrait =
                mContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;

        String oldPackageName = mPackageName;
        mPackageName = screenshot.getPackageNameString();

        mScreenBitmap = screenshot.getBitmap();
        // Optimizations
        mScreenBitmap.setHasAlpha(false);
        mScreenBitmap.prepareToDraw();

        prepareViewForNewScreenshot(screenshot, oldPackageName);

        saveScreenshotInWorkerThread(screenshot.getUserHandle(), finisher,
                this::showUiOnActionsReady, this::showUiOnQuickShareActionReady);

        // The window is focusable by default
        setWindowFocusable(true);
        mScreenshotView.requestFocus();

        enqueueScrollCaptureRequest(screenshot.getUserHandle());

        attachWindow();

        boolean showFlash = true;
        if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE) {
            if (screenshot.getScreenBounds() != null
                    && aspectRatiosMatch(screenshot.getBitmap(), screenshot.getInsets(),
                            screenshot.getScreenBounds())) {
                showFlash = false;
            } else {
                showFlash = true;
                screenshot.setInsets(Insets.NONE);
                screenshot.setScreenBounds(new Rect(0, 0, screenshot.getBitmap().getWidth(),
                        screenshot.getBitmap().getHeight()));
            }
        }

        prepareAnimation(screenshot.getScreenBounds(), showFlash);

        if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
            mScreenshotView.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon(
                    mContext.getDrawable(R.drawable.overlay_badge_background),
                    screenshot.getUserHandle()));
        }
        mScreenshotView.setScreenshot(mScreenBitmap, screenshot.getInsets());
        if (DEBUG_WINDOW) {
            Log.d(TAG, "setContentView: " + mScreenshotView);
        }
        setContentView(mScreenshotView);
        // ignore system bar insets for the purpose of window layout
        mWindow.getDecorView().setOnApplyWindowInsetsListener(
                (v, insets) -> WindowInsets.CONSUMED);
        mScreenshotHandler.cancelTimeout(); // restarted after animation
    }

    void prepareViewForNewScreenshot(ScreenshotData screenshot, String oldPackageName) {
        withWindowAttached(() -> {
            if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)
                    && mUserManager.isManagedProfile(screenshot.getUserHandle().getIdentifier())) {
                mScreenshotView.announceForAccessibility(mContext.getResources().getString(
                        R.string.screenshot_saving_work_profile_title));
            } else {
                mScreenshotView.announceForAccessibility(
                        mContext.getResources().getString(R.string.screenshot_saving_title));
            }
        });

        mScreenshotView.reset();

        if (mScreenshotView.isAttachedToWindow()) {
            // if we didn't already dismiss for another reason
            if (!mScreenshotView.isDismissing()) {
                mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_REENTERED, 0,
                        oldPackageName);
            }
            if (DEBUG_WINDOW) {
                Log.d(TAG, "saveScreenshot: screenshotView is already attached, resetting. "
                        + "(dismissing=" + mScreenshotView.isDismissing() + ")");
            }
        }

        mScreenshotView.setPackageName(mPackageName);

        mScreenshotView.updateOrientation(
                mWindowManager.getCurrentWindowMetrics().getWindowInsets());
    }

    @MainThread
    void takeScreenshotFullscreen(ComponentName topComponent, Consumer<Uri> finisher,
            RequestCallback requestCallback) {
        Assert.isMainThread();
        mCurrentRequestCallback = requestCallback;
        DisplayMetrics displayMetrics = new DisplayMetrics();
        getDefaultDisplay().getRealMetrics(displayMetrics);
        takeScreenshotInternal(
                topComponent, finisher,
                new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels));
        takeScreenshotInternal(topComponent, finisher, getFullScreenRect());
    }

    @MainThread
@@ -641,6 +756,42 @@ public class ScreenshotController {
        setWindowFocusable(true);
        mScreenshotView.requestFocus();

        enqueueScrollCaptureRequest(owner);

        attachWindow();
        prepareAnimation(screenRect, showFlash);

        if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
            mScreenshotView.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon(
                    mContext.getDrawable(R.drawable.overlay_badge_background), owner));
        }
        mScreenshotView.setScreenshot(mScreenBitmap, screenInsets);
        if (DEBUG_WINDOW) {
            Log.d(TAG, "setContentView: " + mScreenshotView);
        }
        setContentView(mScreenshotView);
        // ignore system bar insets for the purpose of window layout
        mWindow.getDecorView().setOnApplyWindowInsetsListener(
                (v, insets) -> WindowInsets.CONSUMED);
        mScreenshotHandler.cancelTimeout(); // restarted after animation
    }

    private void prepareAnimation(Rect screenRect, boolean showFlash) {
        mScreenshotView.getViewTreeObserver().addOnPreDrawListener(
                new ViewTreeObserver.OnPreDrawListener() {
                    @Override
                    public boolean onPreDraw() {
                        if (DEBUG_WINDOW) {
                            Log.d(TAG, "onPreDraw: startAnimation");
                        }
                        mScreenshotView.getViewTreeObserver().removeOnPreDrawListener(this);
                        startAnimation(screenRect, showFlash);
                        return true;
                    }
                });
    }

    private void enqueueScrollCaptureRequest(UserHandle owner) {
        // Wait until this window is attached to request because it is
        // the reference used to locate the target window (below).
        withWindowAttached(() -> {
@@ -678,30 +829,6 @@ public class ScreenshotController {
                        }
                    });
        });

        attachWindow();
        mScreenshotView.getViewTreeObserver().addOnPreDrawListener(
                new ViewTreeObserver.OnPreDrawListener() {
                    @Override
                    public boolean onPreDraw() {
                        if (DEBUG_WINDOW) {
                            Log.d(TAG, "onPreDraw: startAnimation");
                        }
                        mScreenshotView.getViewTreeObserver().removeOnPreDrawListener(this);
                        startAnimation(screenRect, showFlash);
                        return true;
                    }
                });
        if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
            mScreenshotView.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon(
                    mContext.getDrawable(R.drawable.overlay_badge_background), owner));
        }
        mScreenshotView.setScreenshot(mScreenBitmap, screenInsets);

        // ignore system bar insets for the purpose of window layout
        mWindow.getDecorView().setOnApplyWindowInsetsListener(
                (v, insets) -> WindowInsets.CONSUMED);
        mScreenshotHandler.cancelTimeout(); // restarted after animation
    }

    private void requestScrollCapture(UserHandle owner) {
@@ -1154,6 +1281,12 @@ public class ScreenshotController {
        return !mIsLowRamDevice;
    }

    private Rect getFullScreenRect() {
        DisplayMetrics displayMetrics = new DisplayMetrics();
        getDefaultDisplay().getRealMetrics(displayMetrics);
        return new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels);
    }

    /** Does the aspect ratio of the bitmap with insets removed match the bounds. */
    private static boolean aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets,
            Rect screenBounds) {
+43 −0
Original line number Diff line number Diff line
package com.android.systemui.screenshot

import android.content.ComponentName
import android.graphics.Bitmap
import android.graphics.Insets
import android.graphics.Rect
import android.os.UserHandle
import android.view.WindowManager.ScreenshotSource
import android.view.WindowManager.ScreenshotType
import com.android.internal.util.ScreenshotRequest

/** ScreenshotData represents the current state of a single screenshot being acquired. */
data class ScreenshotData(
    @ScreenshotType var type: Int,
    @ScreenshotSource var source: Int,
    /** UserHandle for the owner of the app being screenshotted, if known. */
    var userHandle: UserHandle?,
    /** ComponentName of the top-most app in the screenshot. */
    var topComponent: ComponentName?,
    var screenBounds: Rect?,
    var taskId: Int,
    var insets: Insets,
    var bitmap: Bitmap?,
) {
    val packageNameString: String
        get() = if (topComponent == null) "" else topComponent!!.packageName

    companion object {
        @JvmStatic
        fun fromRequest(request: ScreenshotRequest): ScreenshotData {
            return ScreenshotData(
                request.type,
                request.source,
                if (request.userId >= 0) UserHandle.of(request.userId) else null,
                request.topComponent,
                request.boundsInScreen,
                request.taskId,
                request.insets,
                request.bitmap,
            )
        }
    }
}
+28 −2
Original line number Diff line number Diff line
@@ -59,6 +59,7 @@ import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.FlagListenable.FlagEvent;
import com.android.systemui.flags.Flags;

import java.util.concurrent.Executor;
import java.util.function.Consumer;
@@ -221,9 +222,34 @@ public class TakeScreenshotService extends Service {
            return;
        }

        if (mFeatureFlags.isEnabled(Flags.SCREENSHOT_METADATA)) {
            Log.d(TAG, "Processing screenshot data");
            ScreenshotData screenshotData = ScreenshotData.fromRequest(request);
            mProcessor.processAsync(screenshotData,
                    (data) -> dispatchToController(data, onSaved, callback));
        } else {
            mProcessor.processAsync(request,
                    (r) -> dispatchToController(r, onSaved, callback));
        }
    }

    private void dispatchToController(ScreenshotData screenshot,
            Consumer<Uri> uriConsumer, RequestCallback callback) {

        mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(screenshot.getSource()), 0,
                screenshot.getPackageNameString());

        if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
                && screenshot.getBitmap() == null) {
            Log.e(TAG, "Got null bitmap from screenshot message");
            mNotificationsController.notifyScreenshotError(
                    R.string.screenshot_failed_to_capture_text);
            callback.reportError();
            return;
        }

        mScreenshot.handleScreenshot(screenshot, uriConsumer, callback);
    }

    private void dispatchToController(ScreenshotRequest request,
            Consumer<Uri> uriConsumer, RequestCallback callback) {
Loading