Merge "Return more information from ImageExporter"
This commit is contained in:
committed by
Android (Google) Code Review
commit
3803787444
@@ -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());
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user