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

Commit ddc35cc1 authored by Mark Renouf's avatar Mark Renouf
Browse files

Updates to ImageExporter, adds ImageLoader

Adds ImageLoader which loads bitmaps from MediaStore or direct
from the filesystem asynchronously.

Change-Id: I169325f64892fc0a591238b8cbb8010e863dc0c6
parent 0c7463b8
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";
        });
    }
}