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

Commit 894e86f1 authored by Chen Bai's avatar Chen Bai
Browse files

screenshot: allow customized name for ImageExporter's expoerted file

- Add an interface to allow user customize the name of file output by
  ImageExporter.
  - When there is existing file with the same name and format as the
    newly exported one, the old one will be overrwite by the new.
    Instead of keeping both and add number at the end of the file name.
    (Like, screenshot.png and screenshot (2).png)
- Edited the comment to differentiate existing interface from the new one.

Flag: None
Test: atest ImageExporterTest
Bug: 326477108
Change-Id: I3a5d779f6ec24169c2905f8b78d23aa91c01f7ad
parent ebbf77ea
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,