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

This commit is contained in:
Mark Renouf
2021-03-08 19:35:12 +00:00
committed by Android (Google) Code Review
2 changed files with 251 additions and 89 deletions

View File

@@ -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,88 +285,89 @@ class ImageExporter {
return result;
}
Uri createEntry(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);
if (uri == null) {
throw new ImageExportException(RESOLVER_INSERT_RETURNED_NULL);
}
return uri;
} finally {
Trace.endSection();
}
}
void writeImage(Bitmap bitmap, CompressFormat format, int quality,
Uri contentUri) throws ImageExportException {
Trace.beginSection("ImageExporter_writeImage");
try (OutputStream out = mResolver.openOutputStream(contentUri)) {
long start = SystemClock.elapsedRealtime();
if (!bitmap.compress(format, quality, out)) {
throw new ImageExportException(IMAGE_COMPRESS_RETURNED_FALSE);
} else if (LogConfig.DEBUG_STORAGE) {
Log.d(TAG, "Bitmap.compress took "
+ (SystemClock.elapsedRealtime() - start) + " ms");
}
} catch (IOException ex) {
throw new ImageExportException(OPEN_OUTPUT_STREAM_EXCEPTION, ex);
} finally {
Trace.endSection();
}
}
void writeExif(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);
if (pfd == null) {
throw new ImageExportException(RESOLVER_OPEN_FILE_RETURNED_NULL);
}
ExifInterface exif;
try {
exif = new ExifInterface(pfd.getFileDescriptor());
} catch (IOException e) {
throw new ImageExportException(EXIF_READ_EXCEPTION, e);
}
updateExifAttributes(exif, requestId, width, height, captureTime);
try {
exif.saveAttributes();
} catch (IOException e) {
throw new ImageExportException(EXIF_WRITE_EXCEPTION, e);
}
} catch (FileNotFoundException e) {
throw new ImageExportException(RESOLVER_OPEN_FILE_EXCEPTION, e);
} finally {
closeQuietly(pfd);
Trace.endSection();
}
}
void publishEntry(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);
if (rowsUpdated < 1) {
throw new ImageExportException(RESOLVER_UPDATE_ZERO_ROWS);
}
} finally {
Trace.endSection();
}
}
@Override
public String toString() {
return "compress [" + mBitmap + "] to [" + mFormat + "] at quality " + mQuality;
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 = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
if (uri == null) {
throw new ImageExportException(RESOLVER_INSERT_RETURNED_NULL);
}
return uri;
} finally {
Trace.endSection();
}
}
private static void writeImage(ContentResolver resolver, Bitmap bitmap, CompressFormat format,
int quality, Uri contentUri) throws ImageExportException {
Trace.beginSection("ImageExporter_writeImage");
try (OutputStream out = resolver.openOutputStream(contentUri)) {
long start = SystemClock.elapsedRealtime();
if (!bitmap.compress(format, quality, out)) {
throw new ImageExportException(IMAGE_COMPRESS_RETURNED_FALSE);
} else if (LogConfig.DEBUG_STORAGE) {
Log.d(TAG, "Bitmap.compress took "
+ (SystemClock.elapsedRealtime() - start) + " ms");
}
} catch (IOException ex) {
throw new ImageExportException(OPEN_OUTPUT_STREAM_EXCEPTION, ex);
} finally {
Trace.endSection();
}
}
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 = resolver.openFile(uri, "rw", null);
if (pfd == null) {
throw new ImageExportException(RESOLVER_OPEN_FILE_RETURNED_NULL);
}
ExifInterface exif;
try {
exif = new ExifInterface(pfd.getFileDescriptor());
} catch (IOException e) {
throw new ImageExportException(EXIF_READ_EXCEPTION, e);
}
updateExifAttributes(exif, requestId, width, height, captureTime);
try {
exif.saveAttributes();
} catch (IOException e) {
throw new ImageExportException(EXIF_WRITE_EXCEPTION, e);
}
} catch (FileNotFoundException e) {
throw new ImageExportException(RESOLVER_OPEN_FILE_EXCEPTION, e);
} finally {
closeQuietly(pfd);
Trace.endSection();
}
}
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 = resolver.update(uri, values, /* extras */ null);
if (rowsUpdated < 1) {
throw new ImageExportException(RESOLVER_UPDATE_ZERO_ROWS);
}
} finally {
Trace.endSection();
}
}

View File

@@ -0,0 +1,94 @@
/*
* 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";
});
}
}