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

Commit 38037874 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Return more information from ImageExporter"

parents 4469d60c 3ae3b836
Loading
Loading
Loading
Loading
+41 −13
Original line number Diff line number Diff line
@@ -35,6 +35,8 @@ import android.util.Log;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.exifinterface.media.ExifInterface;

import com.android.internal.annotations.VisibleForTesting;

import com.google.common.util.concurrent.ListenableFuture;

import java.io.File;
@@ -114,8 +116,8 @@ class ImageExporter {
     *
     * @return a listenable future result
     */
    ListenableFuture<Uri> export(Executor executor, Bitmap bitmap) {
        return export(executor, bitmap, ZonedDateTime.now());
    ListenableFuture<Result> export(Executor executor, String requestId, Bitmap bitmap) {
        return export(executor, requestId, bitmap, ZonedDateTime.now());
    }

    /**
@@ -126,8 +128,10 @@ class ImageExporter {
     *
     * @return a listenable future result
     */
    ListenableFuture<Uri> export(Executor executor, Bitmap bitmap, ZonedDateTime captureTime) {
        final Task task = new Task(mResolver, bitmap, captureTime, mCompressFormat, mQuality);
    ListenableFuture<Result> export(Executor executor, String requestId, Bitmap bitmap,
            ZonedDateTime captureTime) {
        final Task task =
                new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat, mQuality);
        return CallbackToFutureAdapter.getFuture(
                (completer) -> {
                    executor.execute(() -> {
@@ -142,32 +146,46 @@ class ImageExporter {
        );
    }

    static class Result {
        String requestId;
        String fileName;
        long timestamp;
        Uri uri;
        CompressFormat format;
    }

    private static class Task {
        private final ContentResolver mResolver;
        private final String mRequestId;
        private final Bitmap mBitmap;
        private final ZonedDateTime mCaptureTime;
        private final CompressFormat mFormat;
        private final int mQuality;
        private final Bitmap mBitmap;
        private final String mFileName;

        Task(ContentResolver resolver, Bitmap bitmap, ZonedDateTime captureTime,
        Task(ContentResolver resolver, String requestId, Bitmap bitmap, ZonedDateTime captureTime,
                CompressFormat format, int quality) {
            mResolver = resolver;
            mRequestId = requestId;
            mBitmap = bitmap;
            mCaptureTime = captureTime;
            mFormat = format;
            mQuality = quality;
            mFileName = createFilename(mCaptureTime, mFormat);
        }

        public Uri execute() throws ImageExportException, InterruptedException {
        public Result execute() throws ImageExportException, InterruptedException {
            Trace.beginSection("ImageExporter_execute");
            Uri uri = null;
            Instant start = null;
            Result result = new Result();
            try {
                if (LogConfig.DEBUG_STORAGE) {
                    Log.d(TAG, "image export started");
                    start = Instant.now();
                }
                uri = createEntry(mFormat, mCaptureTime);

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

                writeImage(mBitmap, mFormat, mQuality, uri);
@@ -178,6 +196,12 @@ class ImageExporter {

                publishEntry(uri);

                result.timestamp = mCaptureTime.toInstant().toEpochMilli();
                result.requestId = mRequestId;
                result.uri = uri;
                result.fileName = mFileName;
                result.format = mFormat;

                if (LogConfig.DEBUG_STORAGE) {
                    Log.d(TAG, "image export completed: "
                            + Duration.between(start, Instant.now()).toMillis() + " ms");
@@ -190,13 +214,15 @@ class ImageExporter {
            } finally {
                Trace.endSection();
            }
            return uri;
            return result;
        }

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

                Uri uri = mResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
                if (uri == null) {
                    throw new ImageExportException(RESOLVER_INSERT_RETURNED_NULL);
@@ -276,14 +302,16 @@ class ImageExporter {
        }
    }

    @VisibleForTesting
    static String createFilename(ZonedDateTime time, CompressFormat format) {
        return String.format(FILENAME_PATTERN, time, fileExtension(format));
    }

    static ContentValues createMetadata(ZonedDateTime captureTime, CompressFormat format) {
    static ContentValues createMetadata(ZonedDateTime captureTime, CompressFormat format,
            String fileName) {
        ContentValues values = new ContentValues();
        values.put(MediaStore.MediaColumns.RELATIVE_PATH, SCREENSHOTS_PATH);
        values.put(MediaStore.MediaColumns.DISPLAY_NAME, createFilename(captureTime, format));
        values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
        values.put(MediaStore.MediaColumns.MIME_TYPE, getMimeType(format));
        values.put(MediaStore.MediaColumns.DATE_ADDED, captureTime.toEpochSecond());
        values.put(MediaStore.MediaColumns.DATE_MODIFIED, captureTime.toEpochSecond());
+25 −20
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import android.content.ClipDescription;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.Icon;
@@ -49,8 +50,9 @@ import com.android.systemui.R;
import com.android.systemui.SystemUIFactory;
import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;

import com.google.common.util.concurrent.ListenableFuture;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@@ -65,7 +67,6 @@ import java.util.function.Supplier;
class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
    private static final String TAG = logTag(SaveImageInBackgroundTask.class);

    private static final String SCREENSHOT_FILE_NAME_TEMPLATE = "Screenshot_%s.png";
    private static final String SCREENSHOT_ID_TEMPLATE = "Screenshot_%s";
    private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)";

@@ -73,14 +74,14 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
    private final ScreenshotSmartActions mScreenshotSmartActions;
    private final ScreenshotController.SaveImageInBackgroundData mParams;
    private final ScreenshotController.SavedImageData mImageData;
    private final String mImageFileName;
    private final long mImageTime;

    private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider;
    private final String mScreenshotId;
    private String mScreenshotId;
    private final boolean mSmartActionsEnabled;
    private final Random mRandom = new Random();
    private final Supplier<ActionTransition> mSharedElementTransition;
    private final ImageExporter mImageExporter;
    private long mImageTime;

    SaveImageInBackgroundTask(Context context, ImageExporter exporter,
            ScreenshotSmartActions screenshotSmartActions,
@@ -94,10 +95,6 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {

        // Prepare all the output metadata
        mParams = data;
        mImageTime = System.currentTimeMillis();
        String imageDate = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(mImageTime));
        mImageFileName = String.format(SCREENSHOT_FILE_NAME_TEMPLATE, imageDate);
        mScreenshotId = String.format(SCREENSHOT_ID_TEMPLATE, UUID.randomUUID());

        // Initialize screenshot notification smart actions provider.
        mSmartActionsEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
@@ -121,18 +118,27 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
            }
            return null;
        }
        // TODO: move to constructor / from ScreenshotRequest
        final UUID uuid = UUID.randomUUID();
        final UserHandle user = getUserHandleOfForegroundApplication(mContext);

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

        Bitmap image = mParams.image;

        String requestId = uuid.toString();
        mScreenshotId = String.format(SCREENSHOT_ID_TEMPLATE, uuid);
        try {
            // Call synchronously here since already on a background thread.
            Uri uri = mImageExporter.export(Runnable::run, image).get();
            ListenableFuture<ImageExporter.Result> future =
                    mImageExporter.export(Runnable::run, requestId, image);
            ImageExporter.Result result = future.get();
            final Uri uri = result.uri;
            mImageTime = result.timestamp;

            CompletableFuture<List<Notification.Action>> smartActionsFuture =
                    mScreenshotSmartActions.getSmartActionsFuture(
                            mScreenshotId, uri, image, mSmartActionsProvider,
                            mSmartActionsEnabled, getUserHandle(mContext));
                            mSmartActionsEnabled, user);

            List<Notification.Action> smartActions = new ArrayList<>();
            if (mSmartActionsEnabled) {
@@ -336,22 +342,21 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
        return deleteActionBuilder.build();
    }

    private int getUserHandleOfForegroundApplication(Context context) {
    private UserHandle getUserHandleOfForegroundApplication(Context context) {
        UserManager manager = UserManager.get(context);
        int result;
        // This logic matches
        // com.android.systemui.statusbar.phone.PhoneStatusBarPolicy#updateManagedProfile
        try {
            return ActivityTaskManager.getService().getLastResumedActivityUserId();
            result = ActivityTaskManager.getService().getLastResumedActivityUserId();
        } catch (RemoteException e) {
            if (DEBUG_ACTIONS) {
                Log.d(TAG, "Failed to get UserHandle of foreground app: ", e);
            }
            return context.getUserId();
        }
            result = context.getUserId();
        }

    private UserHandle getUserHandle(Context context) {
        UserManager manager = UserManager.get(context);
        return manager.getUserInfo(getUserHandleOfForegroundApplication(context)).getUserHandle();
        UserInfo userInfo = manager.getUserInfo(result);
        return userInfo.getUserHandle();
    }

    private List<Notification.Action> buildSmartActions(
+11 −3
Original line number Diff line number Diff line
@@ -29,6 +29,8 @@ import com.android.systemui.screenshot.ScrollCaptureClient.Session;

import com.google.common.util.concurrent.ListenableFuture;

import java.time.ZonedDateTime;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
@@ -52,6 +54,9 @@ public class ScrollCaptureController {
    private final ImageExporter mImageExporter;
    private final ImageTileSet mImageTileSet;

    private ZonedDateTime mCaptureTime;
    private String mRequestId;

    public ScrollCaptureController(Context context, Connection connection, Executor uiExecutor,
            Executor bgExecutor, ImageExporter exporter) {
        mContext = context;
@@ -68,6 +73,8 @@ public class ScrollCaptureController {
     * @param after action to take after the flow is complete
     */
    public void run(final Runnable after) {
        mCaptureTime = ZonedDateTime.now();
        mRequestId = UUID.randomUUID().toString();
        mConnection.start((session) -> startCapture(session, after));
    }

@@ -109,11 +116,12 @@ public class ScrollCaptureController {
    void exportToFile(Bitmap bitmap, Session session, Runnable afterEnd) {
        mImageExporter.setFormat(Bitmap.CompressFormat.PNG);
        mImageExporter.setQuality(6);
        ListenableFuture<Uri> future =
                mImageExporter.export(mBgExecutor, bitmap);
        ListenableFuture<ImageExporter.Result> future =
                mImageExporter.export(mBgExecutor, mRequestId, bitmap, mCaptureTime);
        future.addListener(() -> {
            try {
                launchViewer(future.get());
                ImageExporter.Result result = future.get();
                launchViewer(result.uri);
            } catch (InterruptedException | ExecutionException e) {
                Toast.makeText(mContext, "Failed to write image", Toast.LENGTH_SHORT).show();
                Log.e(TAG, "Error storing screenshot to media store", e.getCause());
+18 −8
Original line number Diff line number Diff line
@@ -32,7 +32,6 @@ import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
@@ -93,21 +92,31 @@ public class ImageExporterTest extends SysuiTestCase {
        ContentResolver contentResolver = context.getContentResolver();
        ImageExporter exporter = new ImageExporter(contentResolver);

        String requestId = "some_random_unique_id";
        Bitmap original = createCheckerBitmap(10, 10, 10);

        ListenableFuture<Uri> direct = exporter.export(DIRECT_EXECUTOR, original, CAPTURE_TIME);
        ListenableFuture<ImageExporter.Result> direct =
                exporter.export(DIRECT_EXECUTOR, requestId, original, CAPTURE_TIME);
        assertTrue("future should be done", direct.isDone());
        assertFalse("future should not be canceled", direct.isCancelled());
        Uri result = direct.get();
        ImageExporter.Result result = direct.get();

        assertEquals("Result should contain the same request id", requestId, result.requestId);
        assertEquals("Filename should contain the correct filename",
                "Screenshot_20201215-131500.png", result.fileName);
        assertNotNull("CompressFormat should be set", result.format);
        assertEquals("The default CompressFormat should be PNG", CompressFormat.PNG, result.format);
        assertNotNull("Uri should not be null", result.uri);
        assertEquals("Timestamp should match input", CAPTURE_TIME.toInstant().toEpochMilli(),
                result.timestamp);

        assertNotNull("Uri should not be null", result);
        Bitmap decoded = null;
        try (InputStream in = contentResolver.openInputStream(result)) {
        try (InputStream in = contentResolver.openInputStream(result.uri)) {
            decoded = BitmapFactory.decodeStream(in);
            assertNotNull("decoded image should not be null", decoded);
            assertTrue("original and decoded image should be identical", original.sameAs(decoded));

            try (ParcelFileDescriptor pfd = contentResolver.openFile(result, "r", null)) {
            try (ParcelFileDescriptor pfd = contentResolver.openFile(result.uri, "r", null)) {
                assertNotNull(pfd);
                ExifInterface exifInterface = new ExifInterface(pfd.getFileDescriptor());

@@ -130,13 +139,14 @@ public class ImageExporterTest extends SysuiTestCase {
            if (decoded != null) {
                decoded.recycle();
            }
            contentResolver.delete(result, null);
            contentResolver.delete(result.uri, null);
        }
    }

    @Test
    public void testMediaStoreMetadata() {
        ContentValues values = ImageExporter.createMetadata(CAPTURE_TIME, CompressFormat.PNG);
        String name = ImageExporter.createFilename(CAPTURE_TIME, CompressFormat.PNG);
        ContentValues values = ImageExporter.createMetadata(CAPTURE_TIME, CompressFormat.PNG, name);
        assertEquals("Pictures/Screenshots",
                values.getAsString(MediaStore.MediaColumns.RELATIVE_PATH));
        assertEquals("Screenshot_20201215-131500.png",