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

Commit cd70ab7a authored by Chen Bai's avatar Chen Bai Committed by Android (Google) Code Review
Browse files

Merge "screenshot: allow customized name for ImageExporter's expoerted file" into main

parents e1da4f65 894e86f1
Loading
Loading
Loading
Loading
+125 −17
Original line number Diff line number Diff line
@@ -21,7 +21,9 @@ import static android.os.FileUtils.closeQuietly;
import android.annotation.IntRange;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.net.Uri;
@@ -50,6 +52,7 @@ import java.io.IOException;
import java.io.OutputStream;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
@@ -145,7 +148,8 @@ public class ImageExporter {
    }

    /**
     * Export the image using the given executor.
     * Export the image using the given executor with an auto-generated file name based on display
     * id.
     *
     * @param executor  the thread for execution
     * @param bitmap    the bitmap to export
@@ -154,23 +158,74 @@ public class ImageExporter {
     */
    public ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap,
            UserHandle owner, int displayId) {
        return export(executor, requestId, bitmap, ZonedDateTime.now(), owner, displayId);
        ZonedDateTime captureTime = ZonedDateTime.now(ZoneId.systemDefault());
        return export(executor,
                new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat,
                        mQuality, /* publish */ true, owner, mFlags,
                        createFilename(captureTime, mCompressFormat, displayId)));
    }

    /**
     * Export the image to MediaStore and publish.
     * Export the image using the given executor with a specified file name.
     *
     * @param executor the thread for execution
     * @param bitmap   the bitmap to export
     * @param format   the compress format of {@code bitmap} e.g. {@link CompressFormat.PNG}
     * @param fileName a specified name for the exported file. No need to include file extension in
     *                 file name. The extension will be internally appended based on
     *                 {@code format}
     * @return a listenable future result
     */
    public ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap,
            CompressFormat format, UserHandle owner, String fileName) {
        return export(executor,
                new Task(mResolver,
                        requestId,
                        bitmap,
                        ZonedDateTime.now(ZoneId.systemDefault()),
                        format,
                        mQuality, /* publish */ true, owner, mFlags,
                        createSystemFileDisplayName(fileName, format),
                        true /* allowOverwrite */));
    }

    /**
     * Export the image to MediaStore and publish.
     *
     * @param executor the thread for execution
     * @param bitmap   the bitmap to export
     * @return a listenable future result
     */
    ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap,
            ZonedDateTime captureTime, UserHandle owner, int displayId) {
        return export(executor, new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat,
                mQuality, /* publish */ true, owner, mFlags,
                createFilename(captureTime, mCompressFormat, displayId)));
    }

        final Task task = new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat,
                mQuality, /* publish */ true, owner, mFlags, displayId);
    /**
     * Export the image to MediaStore and publish.
     *
     * @param executor the thread for execution
     * @param bitmap   the bitmap to export
     * @return a listenable future result
     */
    ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap,
            ZonedDateTime captureTime, UserHandle owner, String fileName) {
        return export(executor, new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat,
                mQuality, /* publish */ true, owner, mFlags,
                createSystemFileDisplayName(fileName, mCompressFormat)));
    }

    /**
     * Export the image to MediaStore and publish.
     *
     * @param executor the thread for execution
     * @param task the exporting image {@link Task}.
     *
     * @return a listenable future result
     */
    private ListenableFuture<Result> export(Executor executor, Task task) {
        return CallbackToFutureAdapter.getFuture(
                (completer) -> {
                    executor.execute(() -> {
@@ -220,9 +275,25 @@ public class ImageExporter {
        private final boolean mPublish;
        private final FeatureFlags mFlags;

        /**
         * This variable specifies the behavior when a file to be exported has a same name and
         * format as one of the file on disk. If this is set to true, the new file overwrite the
         * old file; otherwise, the system adds a number to the end of the newly exported file. For
         * example, if the file is screenshot.png, the newly exported file's display name will be
         * screenshot(1).png.
         */
        private final boolean mAllowOverwrite;

        Task(ContentResolver resolver, UUID requestId, Bitmap bitmap, ZonedDateTime captureTime,
                CompressFormat format, int quality, boolean publish, UserHandle owner,
                FeatureFlags flags, int displayId) {
                FeatureFlags flags, String fileName) {
            this(resolver, requestId, bitmap, captureTime, format, quality, publish, owner, flags,
                    fileName, false /* allowOverwrite */);
        }

        Task(ContentResolver resolver, UUID requestId, Bitmap bitmap, ZonedDateTime captureTime,
                CompressFormat format, int quality, boolean publish, UserHandle owner,
                FeatureFlags flags, String fileName, boolean allowOverwrite) {
            mResolver = resolver;
            mRequestId = requestId;
            mBitmap = bitmap;
@@ -230,9 +301,10 @@ public class ImageExporter {
            mFormat = format;
            mQuality = quality;
            mOwner = owner;
            mFileName = createFilename(mCaptureTime, mFormat, displayId);
            mFileName = fileName;
            mPublish = publish;
            mFlags = flags;
            mAllowOverwrite = allowOverwrite;
        }

        public Result execute() throws ImageExportException, InterruptedException {
@@ -246,7 +318,8 @@ public class ImageExporter {
                    start = Instant.now();
                }

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

                writeImage(mResolver, mBitmap, mFormat, mQuality, uri);
@@ -290,21 +363,51 @@ public class ImageExporter {
    }

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

            Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
            Uri uriWithUserId = ContentProvider.maybeAddUserId(baseUri, owner.getIdentifier());

            Uri uri = resolver.insert(uriWithUserId, values);
            if (uri == null) {
            Uri resultUri = null;

            if (allowOverwrite) {
                // Query to check if there is existing file with the same name and format.
                Cursor cursor = resolver.query(
                        baseUri,
                        null,
                        MediaStore.MediaColumns.DISPLAY_NAME + "=? AND "
                                + MediaStore.MediaColumns.MIME_TYPE + "=?",
                        new String[]{fileName, getMimeType(format)},
                        null /* CancellationSignal */);
                if (cursor != null) {
                    if (cursor.moveToFirst()) {
                        // If there is existing file, update the meta-data of its entry. The Entry's
                        // corresponding uri is composed of volume base-uri(or with user-id) and
                        // its row's unique ID.
                        int idIndex = cursor.getColumnIndex(MediaStore.MediaColumns._ID);
                        resultUri = ContentUris.withAppendedId(uriWithUserId,
                                cursor.getLong(idIndex));
                        resolver.update(resultUri, values, null);
                        Log.d(TAG, "Updated existing URI: " + resultUri);
                    }
                    cursor.close();
                }
            }

            if (resultUri == null) {
                // If file overwriting is disabled or there is no existing file to overwrite, create
                // and insert a new entry.
                resultUri = resolver.insert(uriWithUserId, values);
                Log.d(TAG, "Inserted new URI: " + resultUri);
            }

            if (resultUri == null) {
                throw new ImageExportException(RESOLVER_INSERT_RETURNED_NULL);
            }
            Log.d(TAG, "Inserted new URI: " + uri);
            return uri;
            return resultUri;
        } finally {
            Trace.endSection();
        }
@@ -383,6 +486,11 @@ public class ImageExporter {
            fileExtension(format));
    }

    @VisibleForTesting
    static String createSystemFileDisplayName(String originalDisplayName, CompressFormat format) {
        return originalDisplayName + "." + fileExtension(format);
    }

    static ContentValues createMetadata(ZonedDateTime captureTime, CompressFormat format,
            String fileName) {
        ContentValues values = new ContentValues();
+26 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.systemui.screenshot;

import static com.android.systemui.screenshot.ImageExporter.createSystemFileDisplayName;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -181,6 +183,30 @@ public class ImageExporterTest extends SysuiTestCase {
        }
    }

    @Test
    public void testImageExport_customizedFileName()
            throws ExecutionException, InterruptedException {
        // This test only asserts the file name for the case when user specifies a file name,
        // instead of using the auto-generated name by ImageExporter::createFileName. Other
        // metadata are not affected by the specified file name.
        final String customizedFileName = "customized_file_name";
        ContentResolver contentResolver = mContext.getContentResolver();
        ImageExporter exporter = new ImageExporter(contentResolver, mFeatureFlags);

        UUID requestId = UUID.fromString("3c11da99-9284-4863-b1d5-6f3684976814");
        Bitmap original = createCheckerBitmap(10, 10, 10);

        ListenableFuture<ImageExporter.Result> direct =
                exporter.export(DIRECT_EXECUTOR, requestId, original, CAPTURE_TIME,
                        Process.myUserHandle(), customizedFileName);
        assertTrue("future should be done", direct.isDone());
        assertFalse("future should not be canceled", direct.isCancelled());
        ImageExporter.Result result = direct.get();
        assertEquals("Filename should contain the correct filename",
                createSystemFileDisplayName(customizedFileName, CompressFormat.PNG),
                result.fileName);
    }

    @Test
    public void testMediaStoreMetadata() {
        String name = ImageExporter.createFilename(CAPTURE_TIME, CompressFormat.PNG,