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

Commit cc312a71 authored by Mark Renouf's avatar Mark Renouf Committed by Android (Google) Code Review
Browse files

Merge "Updates to ImageExporter, adds ImageLoader" into sc-dev

parents 3a7b5ffa ddc35cc1
Loading
Loading
Loading
Loading
+145 −77
Original line number Diff line number Diff line
@@ -41,6 +41,7 @@ import com.google.common.util.concurrent.ListenableFuture;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.time.Duration;
@@ -109,6 +110,39 @@ class ImageExporter {
        mQuality = quality;
    }

    /**
     * Stores the given Bitmap to a temp file.
     */
    ListenableFuture<File> exportAsTempFile(Executor executor, Bitmap bitmap) {
        return CallbackToFutureAdapter.getFuture(
                (completer) -> {
                    executor.execute(() -> {
                        File cachePath;
                        try {
                            cachePath = File.createTempFile("long_screenshot_cache_", ".tmp");
                            try (FileOutputStream stream = new FileOutputStream(cachePath)) {
                                bitmap.compress(mCompressFormat, mQuality, stream);
                            } catch (IOException e) {
                                if (cachePath.exists()) {
                                    //noinspection ResultOfMethodCallIgnored
                                    cachePath.delete();
                                    cachePath = null;
                                }
                                completer.setException(e);
                            }
                            if (cachePath != null) {
                                completer.set(cachePath);
                            }
                        } catch (IOException e) {
                            // Failed to create a new file
                            completer.setException(e);
                        }
                    });
                    return "Bitmap#compress";
                }
        );
    }

    /**
     * Export the image using the given executor.
     *
@@ -122,7 +156,7 @@ class ImageExporter {
    }

    /**
     * Export the image using the given executor.
     * Export the image to MediaStore and publish.
     *
     * @param executor the thread for execution
     * @param bitmap the bitmap to export
@@ -131,8 +165,10 @@ class ImageExporter {
     */
    ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap,
            ZonedDateTime captureTime) {
        final Task task =
                new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat, mQuality);

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

        return CallbackToFutureAdapter.getFuture(
                (completer) -> {
                    executor.execute(() -> {
@@ -147,12 +183,36 @@ 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;
        String fileName;
        long timestamp;
        Uri uri;
        CompressFormat format;
        boolean published;
        boolean deleted;
    }

    private static class Task {
@@ -163,9 +223,10 @@ class ImageExporter {
        private final CompressFormat mFormat;
        private final int mQuality;
        private final String mFileName;
        private final boolean mPublish;

        Task(ContentResolver resolver, UUID requestId, Bitmap bitmap, ZonedDateTime captureTime,
                CompressFormat format, int quality) {
                CompressFormat format, int quality, boolean publish) {
            mResolver = resolver;
            mRequestId = requestId;
            mBitmap = bitmap;
@@ -173,6 +234,7 @@ class ImageExporter {
            mFormat = format;
            mQuality = quality;
            mFileName = createFilename(mCaptureTime, mFormat);
            mPublish = publish;
        }

        public Result execute() throws ImageExportException, InterruptedException {
@@ -186,16 +248,21 @@ class ImageExporter {
                    start = Instant.now();
                }

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

                writeImage(mBitmap, mFormat, mQuality, uri);
                writeImage(mResolver, mBitmap, mFormat, mQuality, uri);
                throwIfInterrupted();

                writeExif(uri, mRequestId, mBitmap.getWidth(), mBitmap.getHeight(), mCaptureTime);
                int width = mBitmap.getWidth();
                int height = mBitmap.getHeight();
                writeExif(mResolver, uri, mRequestId, width, height, mCaptureTime);
                throwIfInterrupted();

                publishEntry(uri);
                if (mPublish) {
                    publishEntry(mResolver, uri);
                    result.published = true;
                }

                result.timestamp = mCaptureTime.toInstant().toEpochMilli();
                result.requestId = mRequestId;
@@ -218,13 +285,19 @@ class ImageExporter {
            return result;
        }

        Uri createEntry(CompressFormat format, ZonedDateTime time, String fileName)
                throws ImageExportException {
        @Override
        public String toString() {
            return "export [" + mBitmap + "] to [" + mFormat + "] at quality " + mQuality;
        }
    }

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

                Uri uri = mResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
            Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
            if (uri == null) {
                throw new ImageExportException(RESOLVER_INSERT_RETURNED_NULL);
            }
@@ -234,10 +307,10 @@ class ImageExporter {
        }
    }

        void writeImage(Bitmap bitmap, CompressFormat format, int quality,
                Uri contentUri) throws ImageExportException {
    private static void writeImage(ContentResolver resolver, Bitmap bitmap, CompressFormat format,
            int quality, Uri contentUri) throws ImageExportException {
        Trace.beginSection("ImageExporter_writeImage");
            try (OutputStream out = mResolver.openOutputStream(contentUri)) {
        try (OutputStream out = resolver.openOutputStream(contentUri)) {
            long start = SystemClock.elapsedRealtime();
            if (!bitmap.compress(format, quality, out)) {
                throw new ImageExportException(IMAGE_COMPRESS_RETURNED_FALSE);
@@ -252,12 +325,12 @@ class ImageExporter {
        }
    }

        void writeExif(Uri uri, UUID requestId, int width, int height, ZonedDateTime captureTime)
                throws ImageExportException {
    private static void writeExif(ContentResolver resolver, Uri uri, UUID requestId, int width,
            int height, ZonedDateTime captureTime) throws ImageExportException {
        Trace.beginSection("ImageExporter_writeExif");
        ParcelFileDescriptor pfd = null;
        try {
                pfd = mResolver.openFile(uri, "rw", null);
            pfd = resolver.openFile(uri, "rw", null);
            if (pfd == null) {
                throw new ImageExportException(RESOLVER_OPEN_FILE_RETURNED_NULL);
            }
@@ -282,13 +355,14 @@ class ImageExporter {
        }
    }

        void publishEntry(Uri uri) throws ImageExportException {
    private static void publishEntry(ContentResolver resolver, Uri uri)
            throws ImageExportException {
        Trace.beginSection("ImageExporter_publishEntry");
        try {
            ContentValues values = new ContentValues();
            values.put(MediaStore.MediaColumns.IS_PENDING, 0);
            values.putNull(MediaStore.MediaColumns.DATE_EXPIRES);
                final int rowsUpdated = mResolver.update(uri, values, /* extras */ null);
            final int rowsUpdated = resolver.update(uri, values, /* extras */ null);
            if (rowsUpdated < 1) {
                throw new ImageExportException(RESOLVER_UPDATE_ZERO_ROWS);
            }
@@ -297,12 +371,6 @@ class ImageExporter {
        }
    }

        @Override
        public String toString() {
            return "compress [" + mBitmap + "] to [" + mFormat + "] at quality " + mQuality;
        }
    }

    @VisibleForTesting
    static String createFilename(ZonedDateTime time, CompressFormat format) {
        return String.format(FILENAME_PATTERN, time, fileExtension(format));
+94 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.screenshot;

import android.annotation.Nullable;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.ParcelFileDescriptor;

import androidx.concurrent.futures.CallbackToFutureAdapter;

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

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

import javax.inject.Inject;

/** Loads images. */
public class ImageLoader {
    private final ContentResolver mResolver;

    static class Result {
        @Nullable Uri uri;
        @Nullable File fileName;
        @Nullable Bitmap bitmap;
    }

    @Inject
    ImageLoader(ContentResolver resolver) {
        mResolver = resolver;
    }

    /**
     * Loads an image via URI from ContentResolver.
     *
     * @param uri the identifier of the image to load
     * @return a listenable future result
     */
    ListenableFuture<Result> load(Uri uri) {
        return CallbackToFutureAdapter.getFuture(completer -> {
            Result result = new Result();
            try (InputStream in = mResolver.openInputStream(uri)) {
                result.uri = uri;
                result.bitmap = BitmapFactory.decodeStream(in);
                completer.set(result);
            }
            catch (IOException e) {
                completer.setException(e);
            }
            return "BitmapFactory#decodeStream";
        });
    }

    /**
     * Loads an image by physical filesystem name. The current user must have filesystem
     * permissions to read this file/path.
     *
     * @param file the system file path of the image to load
     * @return a listenable future result
     */
    ListenableFuture<Result> load(File file) {
        return CallbackToFutureAdapter.getFuture(completer -> {
            try (InputStream in = new BufferedInputStream(new FileInputStream(file))) {
                Result result = new Result();
                result.fileName = file;
                result.bitmap = BitmapFactory.decodeStream(in);
                completer.set(result);
            } catch (IOException e) {
                completer.setException(e);
            }
            return "BitmapFactory#decodeStream";
        });
    }
}