Merge "Return more information from ImageExporter"

This commit is contained in:
TreeHugger Robot
2021-01-21 20:05:54 +00:00
committed by Android (Google) Code Review
4 changed files with 95 additions and 44 deletions

View File

@@ -35,6 +35,8 @@ import android.util.Log;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.exifinterface.media.ExifInterface;
import com.android.internal.annotations.VisibleForTesting;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.File;
@@ -114,8 +116,8 @@ class ImageExporter {
*
* @return a listenable future result
*/
ListenableFuture<Uri> export(Executor executor, Bitmap bitmap) {
return export(executor, bitmap, ZonedDateTime.now());
ListenableFuture<Result> export(Executor executor, String requestId, Bitmap bitmap) {
return export(executor, requestId, bitmap, ZonedDateTime.now());
}
/**
@@ -126,8 +128,10 @@ class ImageExporter {
*
* @return a listenable future result
*/
ListenableFuture<Uri> export(Executor executor, Bitmap bitmap, ZonedDateTime captureTime) {
final Task task = new Task(mResolver, bitmap, captureTime, mCompressFormat, mQuality);
ListenableFuture<Result> export(Executor executor, String requestId, Bitmap bitmap,
ZonedDateTime captureTime) {
final Task task =
new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat, mQuality);
return CallbackToFutureAdapter.getFuture(
(completer) -> {
executor.execute(() -> {
@@ -142,32 +146,46 @@ class ImageExporter {
);
}
static class Result {
String requestId;
String fileName;
long timestamp;
Uri uri;
CompressFormat format;
}
private static class Task {
private final ContentResolver mResolver;
private final String mRequestId;
private final Bitmap mBitmap;
private final ZonedDateTime mCaptureTime;
private final CompressFormat mFormat;
private final int mQuality;
private final Bitmap mBitmap;
private final String mFileName;
Task(ContentResolver resolver, Bitmap bitmap, ZonedDateTime captureTime,
Task(ContentResolver resolver, String requestId, Bitmap bitmap, ZonedDateTime captureTime,
CompressFormat format, int quality) {
mResolver = resolver;
mRequestId = requestId;
mBitmap = bitmap;
mCaptureTime = captureTime;
mFormat = format;
mQuality = quality;
mFileName = createFilename(mCaptureTime, mFormat);
}
public Uri execute() throws ImageExportException, InterruptedException {
public Result execute() throws ImageExportException, InterruptedException {
Trace.beginSection("ImageExporter_execute");
Uri uri = null;
Instant start = null;
Result result = new Result();
try {
if (LogConfig.DEBUG_STORAGE) {
Log.d(TAG, "image export started");
start = Instant.now();
}
uri = createEntry(mFormat, mCaptureTime);
uri = createEntry(mFormat, mCaptureTime, mFileName);
throwIfInterrupted();
writeImage(mBitmap, mFormat, mQuality, uri);
@@ -178,6 +196,12 @@ class ImageExporter {
publishEntry(uri);
result.timestamp = mCaptureTime.toInstant().toEpochMilli();
result.requestId = mRequestId;
result.uri = uri;
result.fileName = mFileName;
result.format = mFormat;
if (LogConfig.DEBUG_STORAGE) {
Log.d(TAG, "image export completed: "
+ Duration.between(start, Instant.now()).toMillis() + " ms");
@@ -190,13 +214,15 @@ class ImageExporter {
} finally {
Trace.endSection();
}
return uri;
return result;
}
Uri createEntry(CompressFormat format, ZonedDateTime time) throws ImageExportException {
Uri createEntry(CompressFormat format, ZonedDateTime time, String fileName)
throws ImageExportException {
Trace.beginSection("ImageExporter_createEntry");
try {
final ContentValues values = createMetadata(time, format);
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);
@@ -276,14 +302,16 @@ class ImageExporter {
}
}
@VisibleForTesting
static String createFilename(ZonedDateTime time, CompressFormat format) {
return String.format(FILENAME_PATTERN, time, fileExtension(format));
}
static ContentValues createMetadata(ZonedDateTime captureTime, CompressFormat format) {
static ContentValues createMetadata(ZonedDateTime captureTime, CompressFormat format,
String fileName) {
ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.RELATIVE_PATH, SCREENSHOTS_PATH);
values.put(MediaStore.MediaColumns.DISPLAY_NAME, createFilename(captureTime, format));
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
values.put(MediaStore.MediaColumns.MIME_TYPE, getMimeType(format));
values.put(MediaStore.MediaColumns.DATE_ADDED, captureTime.toEpochSecond());
values.put(MediaStore.MediaColumns.DATE_MODIFIED, captureTime.toEpochSecond());

View File

@@ -29,6 +29,7 @@ import android.content.ClipDescription;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.Icon;
@@ -49,8 +50,9 @@ import com.android.systemui.R;
import com.android.systemui.SystemUIFactory;
import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
import com.google.common.util.concurrent.ListenableFuture;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@@ -65,7 +67,6 @@ import java.util.function.Supplier;
class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
private static final String TAG = logTag(SaveImageInBackgroundTask.class);
private static final String SCREENSHOT_FILE_NAME_TEMPLATE = "Screenshot_%s.png";
private static final String SCREENSHOT_ID_TEMPLATE = "Screenshot_%s";
private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)";
@@ -73,14 +74,14 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
private final ScreenshotSmartActions mScreenshotSmartActions;
private final ScreenshotController.SaveImageInBackgroundData mParams;
private final ScreenshotController.SavedImageData mImageData;
private final String mImageFileName;
private final long mImageTime;
private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider;
private final String mScreenshotId;
private String mScreenshotId;
private final boolean mSmartActionsEnabled;
private final Random mRandom = new Random();
private final Supplier<ActionTransition> mSharedElementTransition;
private final ImageExporter mImageExporter;
private long mImageTime;
SaveImageInBackgroundTask(Context context, ImageExporter exporter,
ScreenshotSmartActions screenshotSmartActions,
@@ -94,10 +95,6 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
// Prepare all the output metadata
mParams = data;
mImageTime = System.currentTimeMillis();
String imageDate = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(mImageTime));
mImageFileName = String.format(SCREENSHOT_FILE_NAME_TEMPLATE, imageDate);
mScreenshotId = String.format(SCREENSHOT_ID_TEMPLATE, UUID.randomUUID());
// Initialize screenshot notification smart actions provider.
mSmartActionsEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
@@ -121,18 +118,27 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
}
return null;
}
// TODO: move to constructor / from ScreenshotRequest
final UUID uuid = UUID.randomUUID();
final UserHandle user = getUserHandleOfForegroundApplication(mContext);
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
Bitmap image = mParams.image;
String requestId = uuid.toString();
mScreenshotId = String.format(SCREENSHOT_ID_TEMPLATE, uuid);
try {
// Call synchronously here since already on a background thread.
Uri uri = mImageExporter.export(Runnable::run, image).get();
ListenableFuture<ImageExporter.Result> future =
mImageExporter.export(Runnable::run, requestId, image);
ImageExporter.Result result = future.get();
final Uri uri = result.uri;
mImageTime = result.timestamp;
CompletableFuture<List<Notification.Action>> smartActionsFuture =
mScreenshotSmartActions.getSmartActionsFuture(
mScreenshotId, uri, image, mSmartActionsProvider,
mSmartActionsEnabled, getUserHandle(mContext));
mSmartActionsEnabled, user);
List<Notification.Action> smartActions = new ArrayList<>();
if (mSmartActionsEnabled) {
@@ -336,22 +342,21 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
return deleteActionBuilder.build();
}
private int getUserHandleOfForegroundApplication(Context context) {
private UserHandle getUserHandleOfForegroundApplication(Context context) {
UserManager manager = UserManager.get(context);
int result;
// This logic matches
// com.android.systemui.statusbar.phone.PhoneStatusBarPolicy#updateManagedProfile
try {
return ActivityTaskManager.getService().getLastResumedActivityUserId();
result = ActivityTaskManager.getService().getLastResumedActivityUserId();
} catch (RemoteException e) {
if (DEBUG_ACTIONS) {
Log.d(TAG, "Failed to get UserHandle of foreground app: ", e);
}
return context.getUserId();
result = context.getUserId();
}
}
private UserHandle getUserHandle(Context context) {
UserManager manager = UserManager.get(context);
return manager.getUserInfo(getUserHandleOfForegroundApplication(context)).getUserHandle();
UserInfo userInfo = manager.getUserInfo(result);
return userInfo.getUserHandle();
}
private List<Notification.Action> buildSmartActions(

View File

@@ -29,6 +29,8 @@ import com.android.systemui.screenshot.ScrollCaptureClient.Session;
import com.google.common.util.concurrent.ListenableFuture;
import java.time.ZonedDateTime;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
@@ -52,6 +54,9 @@ public class ScrollCaptureController {
private final ImageExporter mImageExporter;
private final ImageTileSet mImageTileSet;
private ZonedDateTime mCaptureTime;
private String mRequestId;
public ScrollCaptureController(Context context, Connection connection, Executor uiExecutor,
Executor bgExecutor, ImageExporter exporter) {
mContext = context;
@@ -68,6 +73,8 @@ public class ScrollCaptureController {
* @param after action to take after the flow is complete
*/
public void run(final Runnable after) {
mCaptureTime = ZonedDateTime.now();
mRequestId = UUID.randomUUID().toString();
mConnection.start((session) -> startCapture(session, after));
}
@@ -109,11 +116,12 @@ public class ScrollCaptureController {
void exportToFile(Bitmap bitmap, Session session, Runnable afterEnd) {
mImageExporter.setFormat(Bitmap.CompressFormat.PNG);
mImageExporter.setQuality(6);
ListenableFuture<Uri> future =
mImageExporter.export(mBgExecutor, bitmap);
ListenableFuture<ImageExporter.Result> future =
mImageExporter.export(mBgExecutor, mRequestId, bitmap, mCaptureTime);
future.addListener(() -> {
try {
launchViewer(future.get());
ImageExporter.Result result = future.get();
launchViewer(result.uri);
} catch (InterruptedException | ExecutionException e) {
Toast.makeText(mContext, "Failed to write image", Toast.LENGTH_SHORT).show();
Log.e(TAG, "Error storing screenshot to media store", e.getCause());

View File

@@ -32,7 +32,6 @@ import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
@@ -93,21 +92,31 @@ public class ImageExporterTest extends SysuiTestCase {
ContentResolver contentResolver = context.getContentResolver();
ImageExporter exporter = new ImageExporter(contentResolver);
String requestId = "some_random_unique_id";
Bitmap original = createCheckerBitmap(10, 10, 10);
ListenableFuture<Uri> direct = exporter.export(DIRECT_EXECUTOR, original, CAPTURE_TIME);
ListenableFuture<ImageExporter.Result> direct =
exporter.export(DIRECT_EXECUTOR, requestId, original, CAPTURE_TIME);
assertTrue("future should be done", direct.isDone());
assertFalse("future should not be canceled", direct.isCancelled());
Uri result = direct.get();
ImageExporter.Result result = direct.get();
assertEquals("Result should contain the same request id", requestId, result.requestId);
assertEquals("Filename should contain the correct filename",
"Screenshot_20201215-131500.png", result.fileName);
assertNotNull("CompressFormat should be set", result.format);
assertEquals("The default CompressFormat should be PNG", CompressFormat.PNG, result.format);
assertNotNull("Uri should not be null", result.uri);
assertEquals("Timestamp should match input", CAPTURE_TIME.toInstant().toEpochMilli(),
result.timestamp);
assertNotNull("Uri should not be null", result);
Bitmap decoded = null;
try (InputStream in = contentResolver.openInputStream(result)) {
try (InputStream in = contentResolver.openInputStream(result.uri)) {
decoded = BitmapFactory.decodeStream(in);
assertNotNull("decoded image should not be null", decoded);
assertTrue("original and decoded image should be identical", original.sameAs(decoded));
try (ParcelFileDescriptor pfd = contentResolver.openFile(result, "r", null)) {
try (ParcelFileDescriptor pfd = contentResolver.openFile(result.uri, "r", null)) {
assertNotNull(pfd);
ExifInterface exifInterface = new ExifInterface(pfd.getFileDescriptor());
@@ -130,13 +139,14 @@ public class ImageExporterTest extends SysuiTestCase {
if (decoded != null) {
decoded.recycle();
}
contentResolver.delete(result, null);
contentResolver.delete(result.uri, null);
}
}
@Test
public void testMediaStoreMetadata() {
ContentValues values = ImageExporter.createMetadata(CAPTURE_TIME, CompressFormat.PNG);
String name = ImageExporter.createFilename(CAPTURE_TIME, CompressFormat.PNG);
ContentValues values = ImageExporter.createMetadata(CAPTURE_TIME, CompressFormat.PNG, name);
assertEquals("Pictures/Screenshots",
values.getAsString(MediaStore.MediaColumns.RELATIVE_PATH));
assertEquals("Screenshot_20201215-131500.png",