From f167275f604bdb2d8bf58d35ba3a705111d737d1 Mon Sep 17 00:00:00 2001 From: Mark Renouf Date: Wed, 16 Dec 2020 01:11:04 -0500 Subject: [PATCH] Screenshots: use ImageExporter Use a single tested module for image export steps which is now shared between the standard flow and the long screenshot feature. Test: ImageExporterTest android.assist.cts.ScreenshotTest ScreenshotNotificationSmartActionsTest Bug: 175744393 Fix: 175744393 Change-Id: Id08716736edb4ac1d00c13224e7763e8ca53c979 --- .../systemui/screenshot/ImageExporter.java | 4 - .../screenshot/SaveImageInBackgroundTask.java | 101 +------------ .../screenshot/ScreenshotController.java | 25 +++- .../screenshot/ScrollCaptureController.java | 138 ++++-------------- .../screenshot/ImageExporterTest.java | 9 -- ...creenshotNotificationSmartActionsTest.java | 6 +- 6 files changed, 60 insertions(+), 223 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java index 2719d3e14d334..f77431a7047ef 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java @@ -306,10 +306,6 @@ class ImageExporter { exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, dateTime); exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, subSec); exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, timeZone); - - exif.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, dateTime); - exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, subSec); - exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_DIGITIZED, timeZone); } static String getMimeType(CompressFormat format) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index 334693589503e..db2750b8842f7 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -27,8 +27,6 @@ import android.app.PendingIntent; import android.content.ClipData; import android.content.ClipDescription; import android.content.ComponentName; -import android.content.ContentResolver; -import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.res.Resources; @@ -36,43 +34,26 @@ import android.graphics.Bitmap; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.AsyncTask; -import android.os.Build; import android.os.Bundle; -import android.os.Environment; import android.os.Handler; -import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.provider.DeviceConfig; -import android.provider.MediaStore; -import android.provider.MediaStore.MediaColumns; import android.text.TextUtils; -import android.text.format.DateUtils; import android.util.Log; -import androidx.exifinterface.media.ExifInterface; - import com.android.internal.annotations.VisibleForTesting; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.systemui.R; import com.android.systemui.SystemUIFactory; import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ShareTransition; -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; import java.text.DateFormat; import java.text.SimpleDateFormat; -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Date; import java.util.List; -import java.util.Objects; import java.util.Random; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -99,14 +80,17 @@ class SaveImageInBackgroundTask extends AsyncTask { private final boolean mSmartActionsEnabled; private final Random mRandom = new Random(); private final Supplier mSharedElementTransition; + private final ImageExporter mImageExporter; - SaveImageInBackgroundTask(Context context, ScreenshotSmartActions screenshotSmartActions, + SaveImageInBackgroundTask(Context context, ImageExporter exporter, + ScreenshotSmartActions screenshotSmartActions, ScreenshotController.SaveImageInBackgroundData data, Supplier sharedElementTransition) { mContext = context; mScreenshotSmartActions = screenshotSmartActions; mImageData = new ScreenshotController.SavedImageData(); mSharedElementTransition = sharedElementTransition; + mImageExporter = exporter; // Prepare all the output metadata mParams = data; @@ -139,90 +123,17 @@ class SaveImageInBackgroundTask extends AsyncTask { } Thread.currentThread().setPriority(Thread.MAX_PRIORITY); - ContentResolver resolver = mContext.getContentResolver(); Bitmap image = mParams.image; try { - // Save the screenshot to the MediaStore - final ContentValues values = new ContentValues(); - values.put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES - + File.separator + Environment.DIRECTORY_SCREENSHOTS); - values.put(MediaColumns.DISPLAY_NAME, mImageFileName); - values.put(MediaColumns.MIME_TYPE, "image/png"); - values.put(MediaColumns.DATE_ADDED, mImageTime / 1000); - values.put(MediaColumns.DATE_MODIFIED, mImageTime / 1000); - values.put(MediaColumns.DATE_EXPIRES, (mImageTime + DateUtils.DAY_IN_MILLIS) / 1000); - values.put(MediaColumns.IS_PENDING, 1); - - final Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + // Call synchronously here since already on a background thread. + Uri uri = mImageExporter.export(Runnable::run, image).get(); CompletableFuture> smartActionsFuture = mScreenshotSmartActions.getSmartActionsFuture( mScreenshotId, uri, image, mSmartActionsProvider, mSmartActionsEnabled, getUserHandle(mContext)); - try { - // First, write the actual data for our screenshot - try (OutputStream out = resolver.openOutputStream(uri)) { - if (DEBUG_STORAGE) { - Log.d(TAG, "Compressing PNG:" - + " w=" + image.getWidth() + " h=" + image.getHeight()); - } - if (!image.compress(Bitmap.CompressFormat.PNG, 100, out)) { - if (DEBUG_STORAGE) { - Log.d(TAG, "Bitmap.compress returned false"); - } - throw new IOException("Failed to compress"); - } - if (DEBUG_STORAGE) { - Log.d(TAG, "Done compressing PNG"); - } - } - - // Next, write metadata to help index the screenshot - try (ParcelFileDescriptor pfd = resolver.openFile(uri, "rw", null)) { - final ExifInterface exif = new ExifInterface(pfd.getFileDescriptor()); - - exif.setAttribute(ExifInterface.TAG_SOFTWARE, - "Android " + Build.DISPLAY); - - exif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, - Integer.toString(image.getWidth())); - exif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, - Integer.toString(image.getHeight())); - - final ZonedDateTime time = ZonedDateTime.ofInstant( - Instant.ofEpochMilli(mImageTime), ZoneId.systemDefault()); - exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, - DateTimeFormatter.ofPattern("yyyy:MM:dd HH:mm:ss").format(time)); - exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, - DateTimeFormatter.ofPattern("SSS").format(time)); - - if (Objects.equals(time.getOffset(), ZoneOffset.UTC)) { - exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "+00:00"); - } else { - exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, - DateTimeFormatter.ofPattern("XXX").format(time)); - } - if (DEBUG_STORAGE) { - Log.d(TAG, "Writing EXIF metadata"); - } - exif.saveAttributes(); - } - - // Everything went well above, publish it! - values.clear(); - values.put(MediaColumns.IS_PENDING, 0); - values.putNull(MediaColumns.DATE_EXPIRES); - resolver.update(uri, values, null, null); - if (DEBUG_STORAGE) { - Log.d(TAG, "Completed writing to ContentManager"); - } - } catch (Exception e) { - resolver.delete(uri, null); - throw e; - } - List smartActions = new ArrayList<>(); if (mSmartActionsEnabled) { int timeoutMs = DeviceConfig.getInt( diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index d2fe5d284a97a..a60c24146d9f7 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -78,10 +78,13 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.policy.PhoneWindow; import com.android.settingslib.applications.InterestingConfigChanges; import com.android.systemui.R; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ShareTransition; import com.android.systemui.util.DeviceConfigProxy; import java.util.List; +import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.Supplier; @@ -166,6 +169,9 @@ public class ScreenshotController { private final ScreenshotNotificationsController mNotificationsController; private final ScreenshotSmartActions mScreenshotSmartActions; private final UiEventLogger mUiEventLogger; + private final ImageExporter mImageExporter; + private final Executor mMainExecutor; + private final Executor mBgExecutor; private final WindowManager mWindowManager; private final WindowManager.LayoutParams mWindowLayoutParams; @@ -219,11 +225,17 @@ public class ScreenshotController { ScreenshotNotificationsController screenshotNotificationsController, ScrollCaptureClient scrollCaptureClient, UiEventLogger uiEventLogger, - DeviceConfigProxy configProxy) { + DeviceConfigProxy configProxy, + ImageExporter imageExporter, + @Main Executor mainExecutor, + @Background Executor bgExecutor) { mScreenshotSmartActions = screenshotSmartActions; mNotificationsController = screenshotNotificationsController; mScrollCaptureClient = scrollCaptureClient; mUiEventLogger = uiEventLogger; + mImageExporter = imageExporter; + mMainExecutor = mainExecutor; + mBgExecutor = bgExecutor; final DisplayManager dm = requireNonNull(context.getSystemService(DisplayManager.class)); final Display display = dm.getDisplay(DEFAULT_DISPLAY); @@ -502,9 +514,10 @@ public class ScreenshotController { } } - private void runScrollCapture(ScrollCaptureClient.Connection connection, - Runnable after) { - new ScrollCaptureController(mContext, connection).run(after); + private void runScrollCapture(ScrollCaptureClient.Connection connection, Runnable andThen) { + ScrollCaptureController controller = new ScrollCaptureController(mContext, connection, + mMainExecutor, mBgExecutor, mImageExporter); + controller.run(andThen); } /** @@ -604,8 +617,8 @@ public class ScreenshotController { mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady); } - mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data, - getShareTransitionSupplier()); + mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mImageExporter, + mScreenshotSmartActions, data, getShareTransitionSupplier()); mSaveInBgTask.execute(); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java index 47b4a508f4010..27c74ac0938d7 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java @@ -18,8 +18,6 @@ package com.android.systemui.screenshot; import static android.graphics.ColorSpace.Named.SRGB; -import android.content.ContentResolver; -import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; @@ -27,33 +25,19 @@ import android.graphics.Canvas; import android.graphics.ColorSpace; import android.graphics.Picture; import android.graphics.Rect; -import android.media.ExifInterface; import android.media.Image; import android.net.Uri; -import android.os.Build; -import android.os.Environment; -import android.os.ParcelFileDescriptor; import android.os.UserHandle; -import android.provider.MediaStore; -import android.text.format.DateUtils; import android.util.Log; import android.widget.Toast; import com.android.systemui.screenshot.ScrollCaptureClient.Connection; import com.android.systemui.screenshot.ScrollCaptureClient.Session; -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.sql.Date; -import java.text.SimpleDateFormat; -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Objects; -import java.util.UUID; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; import java.util.function.Consumer; /** @@ -67,11 +51,19 @@ public class ScrollCaptureController { private final Connection mConnection; private final Context mContext; + + private final Executor mUiExecutor; + private final Executor mBgExecutor; + private final ImageExporter mImageExporter; private Picture mPicture; - public ScrollCaptureController(Context context, Connection connection) { + public ScrollCaptureController(Context context, Connection connection, Executor uiExecutor, + Executor bgExecutor, ImageExporter exporter) { mContext = context; mConnection = connection; + mUiExecutor = uiExecutor; + mBgExecutor = bgExecutor; + mImageExporter = exporter; } /** @@ -83,7 +75,7 @@ public class ScrollCaptureController { mConnection.start(MAX_PAGES, (session) -> startCapture(session, after)); } - private void startCapture(Session session, final Runnable after) { + private void startCapture(Session session, final Runnable onDismiss) { Rect requestRect = new Rect(0, 0, session.getMaxTileWidth(), session.getMaxTileHeight()); Consumer consumer = @@ -101,20 +93,11 @@ public class ScrollCaptureController { } if (emptyFrame || mFrameCount >= MAX_PAGES || requestRect.bottom > MAX_HEIGHT) { - Uri uri = null; if (mPicture != null) { - // This is probably on a binder thread right now ¯\_(ツ)_/¯ - uri = writeImage(Bitmap.createBitmap(mPicture)); - // Release those buffers! - mPicture.close(); - } - if (uri != null) { - launchViewer(uri); + exportToFile(mPicture, session, onDismiss); } else { - Toast.makeText(mContext, "Failed to create tall screenshot", - Toast.LENGTH_SHORT).show(); + session.end(onDismiss); } - session.end(after); // end session, close connection, after.run() return; } requestRect.offset(0, session.getMaxTileHeight()); @@ -126,6 +109,22 @@ public class ScrollCaptureController { session.requestTile(requestRect, consumer); }; + void exportToFile(Picture picture, Session session, Runnable afterEnd) { + mImageExporter.setFormat(Bitmap.CompressFormat.PNG); + mImageExporter.setQuality(6); + ListenableFuture future = + mImageExporter.export(mBgExecutor, Bitmap.createBitmap(picture)); + future.addListener(() -> { + picture.close(); // release resources + try { + launchViewer(future.get()); + } 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()); + } + session.end(afterEnd); // end session, close connection, afterEnd.run() + }, mUiExecutor); + } /** * Combine the top {@link Picture} with an {@link Image} by appending the image directly @@ -162,79 +161,6 @@ public class ScrollCaptureController { return combined; } - Uri writeImage(Bitmap image) { - ContentResolver resolver = mContext.getContentResolver(); - long mImageTime = System.currentTimeMillis(); - String imageDate = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(mImageTime)); - String mImageFileName = String.format("tall_Screenshot_%s.png", imageDate); - String mScreenshotId = String.format("Screenshot_%s", UUID.randomUUID()); - try { - // Save the screenshot to the MediaStore - final ContentValues values = new ContentValues(); - values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES - + File.separator + Environment.DIRECTORY_SCREENSHOTS); - values.put(MediaStore.MediaColumns.DISPLAY_NAME, mImageFileName); - values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png"); - values.put(MediaStore.MediaColumns.DATE_ADDED, mImageTime / 1000); - values.put(MediaStore.MediaColumns.DATE_MODIFIED, mImageTime / 1000); - values.put( - MediaStore.MediaColumns.DATE_EXPIRES, - (mImageTime + DateUtils.DAY_IN_MILLIS) / 1000); - values.put(MediaStore.MediaColumns.IS_PENDING, 1); - - final Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - values); - try { - try (OutputStream out = resolver.openOutputStream(uri)) { - if (!image.compress(Bitmap.CompressFormat.PNG, 100, out)) { - throw new IOException("Failed to compress"); - } - } - - // Next, write metadata to help index the screenshot - try (ParcelFileDescriptor pfd = resolver.openFile(uri, "rw", null)) { - final ExifInterface exif = new ExifInterface(pfd.getFileDescriptor()); - - exif.setAttribute(ExifInterface.TAG_SOFTWARE, - "Android " + Build.DISPLAY); - - exif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, - Integer.toString(image.getWidth())); - exif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, - Integer.toString(image.getHeight())); - - final ZonedDateTime time = ZonedDateTime.ofInstant( - Instant.ofEpochMilli(mImageTime), ZoneId.systemDefault()); - exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, - DateTimeFormatter.ofPattern("yyyy:MM:dd HH:mm:ss").format(time)); - exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, - DateTimeFormatter.ofPattern("SSS").format(time)); - - if (Objects.equals(time.getOffset(), ZoneOffset.UTC)) { - exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "+00:00"); - } else { - exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, - DateTimeFormatter.ofPattern("XXX").format(time)); - } - exif.saveAttributes(); - } - - // Everything went well above, publish it! - values.clear(); - values.put(MediaStore.MediaColumns.IS_PENDING, 0); - values.putNull(MediaStore.MediaColumns.DATE_EXPIRES); - resolver.update(uri, values, null, null); - return uri; - } catch (Exception e) { - resolver.delete(uri, null); - throw e; - } - } catch (Exception e) { - Log.e(TAG, "unable to save screenshot", e); - } - return null; - } - void launchViewer(Uri uri) { Intent editIntent = new Intent(Intent.ACTION_VIEW); editIntent.setType("image/png"); 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 f566880ee129e..f2bf7aa3d8422 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java @@ -85,8 +85,6 @@ public class ImageExporterTest extends SysuiTestCase { assertEquals("Exif " + ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "+00:00", exifInterface.getAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL)); - assertEquals("Exif " + ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "+00:00", - exifInterface.getAttribute(ExifInterface.TAG_OFFSET_TIME_DIGITIZED)); } @Test @@ -127,13 +125,6 @@ public class ImageExporterTest extends SysuiTestCase { exifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL)); assertEquals("Exif " + ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "-05:00", exifInterface.getAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL)); - - assertEquals("Exif " + ExifInterface.TAG_DATETIME_DIGITIZED, "2020:12:15 13:15:00", - exifInterface.getAttribute(ExifInterface.TAG_DATETIME_DIGITIZED)); - assertEquals("Exif " + ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, "000", - exifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED)); - assertEquals("Exif " + ExifInterface.TAG_OFFSET_TIME_DIGITIZED, "-05:00", - exifInterface.getAttribute(ExifInterface.TAG_OFFSET_TIME_DIGITIZED)); } } finally { if (decoded != null) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java index 6759c90753567..ced8428e6e6b9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java @@ -176,7 +176,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { data.finisher = null; data.mActionsReadyListener = null; SaveImageInBackgroundTask task = - new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data, + new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data, ShareTransition::new); Notification.Action shareAction = task.createShareAction(mContext, mContext.getResources(), @@ -204,7 +204,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { data.finisher = null; data.mActionsReadyListener = null; SaveImageInBackgroundTask task = - new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data, + new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data, ShareTransition::new); Notification.Action editAction = task.createEditAction(mContext, mContext.getResources(), @@ -232,7 +232,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { data.finisher = null; data.mActionsReadyListener = null; SaveImageInBackgroundTask task = - new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data, + new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data, ShareTransition::new); Notification.Action deleteAction = task.createDeleteAction(mContext,