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

Commit 0236d1c5 authored by Mark Renouf's avatar Mark Renouf Committed by Matt Casey
Browse files

Work profile screenshots: save to owner

When a work profile screenshot is taken, propagate the
owner UserHandle through to the Image export step. This
change will write the image to the correct user profile
and generate a URI with the userId embedded within using
the form:

    content://10@media/external/images/media/000001.png

Since URIs are user-specific, but not unique among users
the userId prefix will carry this information through
to downstream consumers.

The ID can easily be retrieved from the URI using:

    int userId = ContentProvider.getUserIdFromUri(uri)

If there is no userId within the URI, the return value
will be the same as Process.myUserHandle(), the user of
the current process.

Because of this, an explicit UserHandle may not be
needed if this information is conveyed entirely within
the URI (it is included in this CL as-is).

All behavior changes arising from these changes are
currently gated on the SysUi flags:

SCREENSHOT_REQUEST_PROCESSOR
SCREENSHOT_WORK_PROFILE_POLICY

This change does not include support for long screenshots nor fixes for
screenshot actions (share, edit, etc).

Bug: 159422805
Test: atest ImageExporterTest
Change-Id: I641544f6a1b4685c743e7983d0af156e32796f56
parent c71298e6
Loading
Loading
Loading
Loading
+18 −32
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.systemui.screenshot;
import static android.os.FileUtils.closeQuietly;

import android.annotation.IntRange;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.graphics.Bitmap;
@@ -29,6 +30,7 @@ import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import android.os.Trace;
import android.os.UserHandle;
import android.provider.MediaStore;
import android.util.Log;

@@ -142,8 +144,9 @@ class ImageExporter {
     *
     * @return a listenable future result
     */
    ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap) {
        return export(executor, requestId, bitmap, ZonedDateTime.now());
    ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap,
            UserHandle owner) {
        return export(executor, requestId, bitmap, ZonedDateTime.now(), owner);
    }

    /**
@@ -155,10 +158,10 @@ class ImageExporter {
     * @return a listenable future result
     */
    ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap,
            ZonedDateTime captureTime) {
            ZonedDateTime captureTime, UserHandle owner) {

        final Task task = new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat,
                mQuality, /* publish */ true);
                mQuality, /* publish */ true, owner);

        return CallbackToFutureAdapter.getFuture(
                (completer) -> {
@@ -174,28 +177,6 @@ class ImageExporter {
        );
    }

    /**
     * Delete the entry.
     *
     * @param executor the thread for execution
     * @param uri the uri of the image to publish
     *
     * @return a listenable future result
     */
    ListenableFuture<Result> delete(Executor executor, Uri uri) {
        return CallbackToFutureAdapter.getFuture((completer) -> {
            executor.execute(() -> {
                mResolver.delete(uri, null);

                Result result = new Result();
                result.uri = uri;
                result.deleted = true;
                completer.set(result);
            });
            return "ContentResolver#delete";
        });
    }

    static class Result {
        Uri uri;
        UUID requestId;
@@ -203,7 +184,6 @@ class ImageExporter {
        long timestamp;
        CompressFormat format;
        boolean published;
        boolean deleted;

        @Override
        public String toString() {
@@ -214,7 +194,6 @@ class ImageExporter {
            sb.append(", timestamp=").append(timestamp);
            sb.append(", format=").append(format);
            sb.append(", published=").append(published);
            sb.append(", deleted=").append(deleted);
            sb.append('}');
            return sb.toString();
        }
@@ -227,17 +206,19 @@ class ImageExporter {
        private final ZonedDateTime mCaptureTime;
        private final CompressFormat mFormat;
        private final int mQuality;
        private final UserHandle mOwner;
        private final String mFileName;
        private final boolean mPublish;

        Task(ContentResolver resolver, UUID requestId, Bitmap bitmap, ZonedDateTime captureTime,
                CompressFormat format, int quality, boolean publish) {
                CompressFormat format, int quality, boolean publish, UserHandle owner) {
            mResolver = resolver;
            mRequestId = requestId;
            mBitmap = bitmap;
            mCaptureTime = captureTime;
            mFormat = format;
            mQuality = quality;
            mOwner = owner;
            mFileName = createFilename(mCaptureTime, mFormat);
            mPublish = publish;
        }
@@ -253,7 +234,7 @@ class ImageExporter {
                    start = Instant.now();
                }

                uri = createEntry(mResolver, mFormat, mCaptureTime, mFileName);
                uri = createEntry(mResolver, mFormat, mCaptureTime, mFileName, mOwner);
                throwIfInterrupted();

                writeImage(mResolver, mBitmap, mFormat, mQuality, uri);
@@ -297,15 +278,20 @@ class ImageExporter {
    }

    private static Uri createEntry(ContentResolver resolver, CompressFormat format,
            ZonedDateTime time, String fileName) throws ImageExportException {
            ZonedDateTime time, String fileName, UserHandle owner) throws ImageExportException {
        Trace.beginSection("ImageExporter_createEntry");
        try {
            final ContentValues values = createMetadata(time, format, fileName);

            Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
            Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
            if (UserHandle.myUserId() != owner.getIdentifier()) {
                baseUri = ContentProvider.maybeAddUserId(baseUri, owner.getIdentifier());
            }
            Uri uri = resolver.insert(baseUri, values);
            if (uri == null) {
                throw new ImageExportException(RESOLVER_INSERT_RETURNED_NULL);
            }
            Log.d(TAG, "Inserted new URI: " + uri);
            return uri;
        } finally {
            Trace.endSection();
+4 −1
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Process;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
@@ -387,7 +388,9 @@ public class LongScreenshotActivity extends Activity {

        mOutputBitmap = renderBitmap(drawable, bounds);
        ListenableFuture<ImageExporter.Result> exportFuture = mImageExporter.export(
                mBackgroundExecutor, UUID.randomUUID(), mOutputBitmap, ZonedDateTime.now());
                mBackgroundExecutor, UUID.randomUUID(), mOutputBitmap, ZonedDateTime.now(),
                // TODO: Owner must match the owner of the captured window.
                Process.myUserHandle());
        exportFuture.addListener(() -> onExportCompleted(action, exportFuture), mUiExecutor);
    }

+13 −3
Original line number Diff line number Diff line
@@ -48,6 +48,8 @@ import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.systemui.R;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;

import com.google.common.util.concurrent.ListenableFuture;
@@ -71,6 +73,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
    private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)";

    private final Context mContext;
    private FeatureFlags mFlags;
    private final ScreenshotSmartActions mScreenshotSmartActions;
    private final ScreenshotController.SaveImageInBackgroundData mParams;
    private final ScreenshotController.SavedImageData mImageData;
@@ -84,7 +87,10 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
    private final ImageExporter mImageExporter;
    private long mImageTime;

    SaveImageInBackgroundTask(Context context, ImageExporter exporter,
    SaveImageInBackgroundTask(
            Context context,
            FeatureFlags flags,
            ImageExporter exporter,
            ScreenshotSmartActions screenshotSmartActions,
            ScreenshotController.SaveImageInBackgroundData data,
            Supplier<ActionTransition> sharedElementTransition,
@@ -92,6 +98,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
                    screenshotNotificationSmartActionsProvider
    ) {
        mContext = context;
        mFlags = flags;
        mScreenshotSmartActions = screenshotSmartActions;
        mImageData = new ScreenshotController.SavedImageData();
        mQuickShareData = new ScreenshotController.QuickShareData();
@@ -117,7 +124,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
        }
        // TODO: move to constructor / from ScreenshotRequest
        final UUID requestId = UUID.randomUUID();
        final UserHandle user = getUserHandleOfForegroundApplication(mContext);
        final UserHandle user = mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)
                ? mParams.owner : getUserHandleOfForegroundApplication(mContext);

        Thread.currentThread().setPriority(Thread.MAX_PRIORITY);

@@ -133,8 +141,9 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {

            // Call synchronously here since already on a background thread.
            ListenableFuture<ImageExporter.Result> future =
                    mImageExporter.export(Runnable::run, requestId, image);
                    mImageExporter.export(Runnable::run, requestId, image, mParams.owner);
            ImageExporter.Result result = future.get();
            Log.d(TAG, "Saved screenshot: " + result);
            final Uri uri = result.uri;
            mImageTime = result.timestamp;

@@ -157,6 +166,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
            }

            mImageData.uri = uri;
            mImageData.owner = user;
            mImageData.smartActions = smartActions;
            mImageData.shareTransition = createShareAction(mContext, mContext.getResources(), uri);
            mImageData.editTransition = createEditAction(mContext, mContext.getResources(), uri);
+38 −15
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ import static java.util.Objects.requireNonNull;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityOptions;
@@ -57,7 +58,9 @@ import android.media.AudioSystem;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.DisplayMetrics;
import android.util.Log;
@@ -90,6 +93,7 @@ import com.android.systemui.R;
import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.clipboardoverlay.ClipboardOverlayController;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback;
import com.android.systemui.util.Assert;
@@ -151,6 +155,7 @@ public class ScreenshotController {
        public Consumer<Uri> finisher;
        public ScreenshotController.ActionsReadyListener mActionsReadyListener;
        public ScreenshotController.QuickShareActionReadyListener mQuickShareActionsReadyListener;
        public UserHandle owner;

        void clearImage() {
            image = null;
@@ -167,6 +172,8 @@ public class ScreenshotController {
        public Notification.Action deleteAction;
        public List<Notification.Action> smartActions;
        public Notification.Action quickShareAction;
        public UserHandle owner;


        /**
         * POD for shared element transition.
@@ -242,6 +249,7 @@ public class ScreenshotController {
    private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000;

    private final WindowContext mContext;
    private final FeatureFlags mFlags;
    private final ScreenshotNotificationsController mNotificationsController;
    private final ScreenshotSmartActions mScreenshotSmartActions;
    private final UiEventLogger mUiEventLogger;
@@ -288,6 +296,7 @@ public class ScreenshotController {
    @Inject
    ScreenshotController(
            Context context,
            FeatureFlags flags,
            ScreenshotSmartActions screenshotSmartActions,
            ScreenshotNotificationsController screenshotNotificationsController,
            ScrollCaptureClient scrollCaptureClient,
@@ -331,6 +340,7 @@ public class ScreenshotController {
        final Context displayContext = context.createDisplayContext(getDefaultDisplay());
        mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
        mWindowManager = mContext.getSystemService(WindowManager.class);
        mFlags = flags;

        mAccessibilityManager = AccessibilityManager.getInstance(mContext);

@@ -377,7 +387,6 @@ public class ScreenshotController {
    void handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds,
            Insets visibleInsets, int taskId, int userId, ComponentName topComponent,
            Consumer<Uri> finisher, RequestCallback requestCallback) {
        // TODO: use task Id, userId, topComponent for smart handler
        Assert.isMainThread();
        if (screenshot == null) {
            Log.e(TAG, "Got null bitmap from screenshot message");
@@ -395,7 +404,7 @@ public class ScreenshotController {
        }
        mCurrentRequestCallback = requestCallback;
        saveScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, topComponent,
                showFlash);
                showFlash, UserHandle.of(userId));
    }

    /**
@@ -543,14 +552,15 @@ public class ScreenshotController {
            return;
        }

        saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, topComponent, true);
        saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, topComponent, true,
                Process.myUserHandle());

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

    private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect,
            Insets screenInsets, ComponentName topComponent, boolean showFlash) {
            Insets screenInsets, ComponentName topComponent, boolean showFlash, UserHandle owner) {
        withWindowAttached(() ->
                mScreenshotView.announceForAccessibility(
                        mContext.getResources().getString(R.string.screenshot_saving_title)));
@@ -575,11 +585,11 @@ public class ScreenshotController {

        mScreenBitmap = screenshot;

        if (!isUserSetupComplete()) {
        if (!isUserSetupComplete(owner)) {
            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(finisher);
            saveScreenshotAndToast(owner, finisher);
            return;
        }

@@ -587,7 +597,7 @@ public class ScreenshotController {
        mScreenBitmap.setHasAlpha(false);
        mScreenBitmap.prepareToDraw();

        saveScreenshotInWorkerThread(finisher, this::showUiOnActionsReady,
        saveScreenshotInWorkerThread(owner, finisher, this::showUiOnActionsReady,
                this::showUiOnQuickShareActionReady);

        // The window is focusable by default
@@ -853,11 +863,12 @@ public class ScreenshotController {
     * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on
     * failure).
     */
    private void saveScreenshotAndToast(Consumer<Uri> finisher) {
    private void saveScreenshotAndToast(UserHandle owner, Consumer<Uri> finisher) {
        // Play the shutter sound to notify that we've taken a screenshot
        playCameraSound();

        saveScreenshotInWorkerThread(
                owner,
                /* onComplete */ finisher,
                /* actionsReadyListener */ imageData -> {
                    if (DEBUG_CALLBACK) {
@@ -925,9 +936,11 @@ public class ScreenshotController {
    /**
     * Creates a new worker thread and saves the screenshot to the media store.
     */
    private void saveScreenshotInWorkerThread(Consumer<Uri> finisher,
            @Nullable ScreenshotController.ActionsReadyListener actionsReadyListener,
            @Nullable ScreenshotController.QuickShareActionReadyListener
    private void saveScreenshotInWorkerThread(
            UserHandle owner,
            @NonNull Consumer<Uri> finisher,
            @Nullable ActionsReadyListener actionsReadyListener,
            @Nullable QuickShareActionReadyListener
                    quickShareActionsReadyListener) {
        ScreenshotController.SaveImageInBackgroundData
                data = new ScreenshotController.SaveImageInBackgroundData();
@@ -935,13 +948,14 @@ public class ScreenshotController {
        data.finisher = finisher;
        data.mActionsReadyListener = actionsReadyListener;
        data.mQuickShareActionsReadyListener = quickShareActionsReadyListener;
        data.owner = owner;

        if (mSaveInBgTask != null) {
            // just log success/failure for the pre-existing screenshot
            mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady);
        }

        mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mImageExporter,
        mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mFlags, mImageExporter,
                mScreenshotSmartActions, data, getActionTransitionSupplier(),
                mScreenshotNotificationSmartActionsProvider);
        mSaveInBgTask.execute();
@@ -960,6 +974,15 @@ public class ScreenshotController {
        mScreenshotHandler.resetTimeout();

        if (imageData.uri != null) {
            if (!imageData.owner.equals(Process.myUserHandle())) {
                // TODO: Handle non-primary user ownership (e.g. Work Profile)
                // This image is owned by another user. Special treatment will be
                // required in the UI (badging) as well as sending intents which can
                // correctly forward those URIs on to be read (actions).

                Log.d(TAG, "*** Screenshot saved to a non-primary user ("
                        + imageData.owner + ") as " + imageData.uri);
            }
            mScreenshotHandler.post(() -> {
                if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
                    mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
@@ -1033,9 +1056,9 @@ public class ScreenshotController {
        }
    }

    private boolean isUserSetupComplete() {
        return Settings.Secure.getInt(mContext.getContentResolver(),
                SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
    private boolean isUserSetupComplete(UserHandle owner) {
        return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0)
                        .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
    }

    /**
+3 −1
Original line number Diff line number Diff line
@@ -68,7 +68,9 @@ internal open class ScreenshotPolicyImpl @Inject constructor(
    }

    override suspend fun isManagedProfile(@UserIdInt userId: Int): Boolean {
        return withContext(bgDispatcher) { userMgr.isManagedProfile(userId) }
        val managed = withContext(bgDispatcher) { userMgr.isManagedProfile(userId) }
        Log.d(TAG, "isManagedProfile: $managed")
        return managed
    }

    private fun nonPipVisibleTask(info: RootTaskInfo): Boolean {
Loading