Loading packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java +41 −13 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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()); } /** Loading @@ -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(() -> { Loading @@ -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); Loading @@ -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"); Loading @@ -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); Loading Loading @@ -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()); Loading packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +25 −20 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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)"; Loading @@ -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, Loading @@ -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, Loading @@ -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) { Loading Loading @@ -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( Loading packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java +11 −3 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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)); } Loading Loading @@ -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()); Loading packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java +18 −8 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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()); Loading @@ -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", Loading Loading
packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java +41 −13 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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()); } /** Loading @@ -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(() -> { Loading @@ -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); Loading @@ -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"); Loading @@ -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); Loading Loading @@ -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()); Loading
packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +25 −20 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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)"; Loading @@ -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, Loading @@ -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, Loading @@ -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) { Loading Loading @@ -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( Loading
packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java +11 −3 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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)); } Loading Loading @@ -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()); Loading
packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java +18 −8 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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()); Loading @@ -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", Loading