diff options
| author | 2021-01-20 10:15:30 -0500 | |
|---|---|---|
| committer | 2021-01-20 14:04:15 -0500 | |
| commit | 3ae3b8367d9a181872b5f7aeecbf288c28c658ab (patch) | |
| tree | 6959256e7479e03a606dceab1cbef504b81ed01e | |
| parent | ee605b646562fa32cccceb18c610fc8c37df1ed7 (diff) | |
Return more information from ImageExporter
Adds filename, compress format and timestamp
This eliminates additional duplicated code between
SaveImageInBackgroundTask and ImageExporer, related to
determining the filename and the capture time.
Bug: 177996487
Test: atest ImageExporterTest
Change-Id: I794727f8c67bc6b39de81978df0482de67894616
4 files changed, 95 insertions, 44 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java index f77431a7047e..87e236ceb894 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java @@ -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()); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index db2750b8842f..d42847d0afb5 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -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.ShareTransition; +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<ShareTransition> 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) { @@ -331,22 +337,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( diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java index c75efbcc5f80..2ccb2ce0ec48 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java @@ -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()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java index f2bf7aa3d842..c2a08ba54087 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java @@ -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", |