Loading packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java +0 −4 Original line number Diff line number Diff line Loading @@ -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) { Loading packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +6 −95 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -99,14 +80,17 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { private final boolean mSmartActionsEnabled; private final Random mRandom = new Random(); private final Supplier<ShareTransition> mSharedElementTransition; private final ImageExporter mImageExporter; SaveImageInBackgroundTask(Context context, ScreenshotSmartActions screenshotSmartActions, SaveImageInBackgroundTask(Context context, ImageExporter exporter, ScreenshotSmartActions screenshotSmartActions, ScreenshotController.SaveImageInBackgroundData data, Supplier<ShareTransition> sharedElementTransition) { mContext = context; mScreenshotSmartActions = screenshotSmartActions; mImageData = new ScreenshotController.SavedImageData(); mSharedElementTransition = sharedElementTransition; mImageExporter = exporter; // Prepare all the output metadata mParams = data; Loading Loading @@ -139,90 +123,17 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { } 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<List<Notification.Action>> 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<Notification.Action> smartActions = new ArrayList<>(); if (mSmartActionsEnabled) { int timeoutMs = DeviceConfig.getInt( Loading packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +19 −6 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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); Loading Loading @@ -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); } /** Loading Loading @@ -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(); } Loading packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java +32 −106 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; /** Loading @@ -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; } /** Loading @@ -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<ScrollCaptureClient.CaptureResult> consumer = Loading @@ -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()); Loading @@ -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<Uri> 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 Loading Loading @@ -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"); Loading packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java +0 −9 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) { Loading Loading
packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java +0 −4 Original line number Diff line number Diff line Loading @@ -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) { Loading
packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +6 −95 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -99,14 +80,17 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { private final boolean mSmartActionsEnabled; private final Random mRandom = new Random(); private final Supplier<ShareTransition> mSharedElementTransition; private final ImageExporter mImageExporter; SaveImageInBackgroundTask(Context context, ScreenshotSmartActions screenshotSmartActions, SaveImageInBackgroundTask(Context context, ImageExporter exporter, ScreenshotSmartActions screenshotSmartActions, ScreenshotController.SaveImageInBackgroundData data, Supplier<ShareTransition> sharedElementTransition) { mContext = context; mScreenshotSmartActions = screenshotSmartActions; mImageData = new ScreenshotController.SavedImageData(); mSharedElementTransition = sharedElementTransition; mImageExporter = exporter; // Prepare all the output metadata mParams = data; Loading Loading @@ -139,90 +123,17 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { } 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<List<Notification.Action>> 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<Notification.Action> smartActions = new ArrayList<>(); if (mSmartActionsEnabled) { int timeoutMs = DeviceConfig.getInt( Loading
packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +19 −6 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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); Loading Loading @@ -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); } /** Loading Loading @@ -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(); } Loading
packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java +32 −106 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; /** Loading @@ -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; } /** Loading @@ -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<ScrollCaptureClient.CaptureResult> consumer = Loading @@ -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()); Loading @@ -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<Uri> 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 Loading Loading @@ -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"); Loading
packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java +0 −9 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) { Loading