Loading packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java +145 −77 Original line number Diff line number Diff line Loading @@ -41,6 +41,7 @@ import com.google.common.util.concurrent.ListenableFuture; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.time.Duration; Loading Loading @@ -109,6 +110,39 @@ class ImageExporter { mQuality = quality; } /** * Stores the given Bitmap to a temp file. */ ListenableFuture<File> exportAsTempFile(Executor executor, Bitmap bitmap) { return CallbackToFutureAdapter.getFuture( (completer) -> { executor.execute(() -> { File cachePath; try { cachePath = File.createTempFile("long_screenshot_cache_", ".tmp"); try (FileOutputStream stream = new FileOutputStream(cachePath)) { bitmap.compress(mCompressFormat, mQuality, stream); } catch (IOException e) { if (cachePath.exists()) { //noinspection ResultOfMethodCallIgnored cachePath.delete(); cachePath = null; } completer.setException(e); } if (cachePath != null) { completer.set(cachePath); } } catch (IOException e) { // Failed to create a new file completer.setException(e); } }); return "Bitmap#compress"; } ); } /** * Export the image using the given executor. * Loading @@ -122,7 +156,7 @@ class ImageExporter { } /** * Export the image using the given executor. * Export the image to MediaStore and publish. * * @param executor the thread for execution * @param bitmap the bitmap to export Loading @@ -131,8 +165,10 @@ class ImageExporter { */ ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap, ZonedDateTime captureTime) { final Task task = new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat, mQuality); final Task task = new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat, mQuality, /* publish */ true); return CallbackToFutureAdapter.getFuture( (completer) -> { executor.execute(() -> { Loading @@ -147,12 +183,36 @@ class ImageExporter { ); } /** * Delete the entry. * * @param executor the thread for execution * @param uri the uri of the image to publish * * @return a listenable future result */ ListenableFuture<Result> delete(Executor executor, Uri uri) { return CallbackToFutureAdapter.getFuture((completer) -> { executor.execute(() -> { mResolver.delete(uri, null); Result result = new Result(); result.uri = uri; result.deleted = true; completer.set(result); }); return "ContentResolver#delete"; }); } static class Result { Uri uri; UUID requestId; String fileName; long timestamp; Uri uri; CompressFormat format; boolean published; boolean deleted; } private static class Task { Loading @@ -163,9 +223,10 @@ class ImageExporter { private final CompressFormat mFormat; private final int mQuality; private final String mFileName; private final boolean mPublish; Task(ContentResolver resolver, UUID requestId, Bitmap bitmap, ZonedDateTime captureTime, CompressFormat format, int quality) { CompressFormat format, int quality, boolean publish) { mResolver = resolver; mRequestId = requestId; mBitmap = bitmap; Loading @@ -173,6 +234,7 @@ class ImageExporter { mFormat = format; mQuality = quality; mFileName = createFilename(mCaptureTime, mFormat); mPublish = publish; } public Result execute() throws ImageExportException, InterruptedException { Loading @@ -186,16 +248,21 @@ class ImageExporter { start = Instant.now(); } uri = createEntry(mFormat, mCaptureTime, mFileName); uri = createEntry(mResolver, mFormat, mCaptureTime, mFileName); throwIfInterrupted(); writeImage(mBitmap, mFormat, mQuality, uri); writeImage(mResolver, mBitmap, mFormat, mQuality, uri); throwIfInterrupted(); writeExif(uri, mRequestId, mBitmap.getWidth(), mBitmap.getHeight(), mCaptureTime); int width = mBitmap.getWidth(); int height = mBitmap.getHeight(); writeExif(mResolver, uri, mRequestId, width, height, mCaptureTime); throwIfInterrupted(); publishEntry(uri); if (mPublish) { publishEntry(mResolver, uri); result.published = true; } result.timestamp = mCaptureTime.toInstant().toEpochMilli(); result.requestId = mRequestId; Loading @@ -218,13 +285,19 @@ class ImageExporter { return result; } Uri createEntry(CompressFormat format, ZonedDateTime time, String fileName) throws ImageExportException { @Override public String toString() { return "export [" + mBitmap + "] to [" + mFormat + "] at quality " + mQuality; } } private static Uri createEntry(ContentResolver resolver, CompressFormat format, ZonedDateTime time, String fileName) throws ImageExportException { Trace.beginSection("ImageExporter_createEntry"); try { final ContentValues values = createMetadata(time, format, fileName); Uri uri = mResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); if (uri == null) { throw new ImageExportException(RESOLVER_INSERT_RETURNED_NULL); } Loading @@ -234,10 +307,10 @@ class ImageExporter { } } void writeImage(Bitmap bitmap, CompressFormat format, int quality, Uri contentUri) throws ImageExportException { private static void writeImage(ContentResolver resolver, Bitmap bitmap, CompressFormat format, int quality, Uri contentUri) throws ImageExportException { Trace.beginSection("ImageExporter_writeImage"); try (OutputStream out = mResolver.openOutputStream(contentUri)) { try (OutputStream out = resolver.openOutputStream(contentUri)) { long start = SystemClock.elapsedRealtime(); if (!bitmap.compress(format, quality, out)) { throw new ImageExportException(IMAGE_COMPRESS_RETURNED_FALSE); Loading @@ -252,12 +325,12 @@ class ImageExporter { } } void writeExif(Uri uri, UUID requestId, int width, int height, ZonedDateTime captureTime) throws ImageExportException { private static void writeExif(ContentResolver resolver, Uri uri, UUID requestId, int width, int height, ZonedDateTime captureTime) throws ImageExportException { Trace.beginSection("ImageExporter_writeExif"); ParcelFileDescriptor pfd = null; try { pfd = mResolver.openFile(uri, "rw", null); pfd = resolver.openFile(uri, "rw", null); if (pfd == null) { throw new ImageExportException(RESOLVER_OPEN_FILE_RETURNED_NULL); } Loading @@ -282,13 +355,14 @@ class ImageExporter { } } void publishEntry(Uri uri) throws ImageExportException { private static void publishEntry(ContentResolver resolver, Uri uri) throws ImageExportException { Trace.beginSection("ImageExporter_publishEntry"); try { ContentValues values = new ContentValues(); values.put(MediaStore.MediaColumns.IS_PENDING, 0); values.putNull(MediaStore.MediaColumns.DATE_EXPIRES); final int rowsUpdated = mResolver.update(uri, values, /* extras */ null); final int rowsUpdated = resolver.update(uri, values, /* extras */ null); if (rowsUpdated < 1) { throw new ImageExportException(RESOLVER_UPDATE_ZERO_ROWS); } Loading @@ -297,12 +371,6 @@ class ImageExporter { } } @Override public String toString() { return "compress [" + mBitmap + "] to [" + mFormat + "] at quality " + mQuality; } } @VisibleForTesting static String createFilename(ZonedDateTime time, CompressFormat format) { return String.format(FILENAME_PATTERN, time, fileExtension(format)); Loading packages/SystemUI/src/com/android/systemui/screenshot/ImageLoader.java 0 → 100644 +94 −0 Original line number Diff line number Diff line /* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.screenshot; import android.annotation.Nullable; import android.content.ContentResolver; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.ParcelFileDescriptor; import androidx.concurrent.futures.CallbackToFutureAdapter; import com.google.common.util.concurrent.ListenableFuture; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import javax.inject.Inject; /** Loads images. */ public class ImageLoader { private final ContentResolver mResolver; static class Result { @Nullable Uri uri; @Nullable File fileName; @Nullable Bitmap bitmap; } @Inject ImageLoader(ContentResolver resolver) { mResolver = resolver; } /** * Loads an image via URI from ContentResolver. * * @param uri the identifier of the image to load * @return a listenable future result */ ListenableFuture<Result> load(Uri uri) { return CallbackToFutureAdapter.getFuture(completer -> { Result result = new Result(); try (InputStream in = mResolver.openInputStream(uri)) { result.uri = uri; result.bitmap = BitmapFactory.decodeStream(in); completer.set(result); } catch (IOException e) { completer.setException(e); } return "BitmapFactory#decodeStream"; }); } /** * Loads an image by physical filesystem name. The current user must have filesystem * permissions to read this file/path. * * @param file the system file path of the image to load * @return a listenable future result */ ListenableFuture<Result> load(File file) { return CallbackToFutureAdapter.getFuture(completer -> { try (InputStream in = new BufferedInputStream(new FileInputStream(file))) { Result result = new Result(); result.fileName = file; result.bitmap = BitmapFactory.decodeStream(in); completer.set(result); } catch (IOException e) { completer.setException(e); } return "BitmapFactory#decodeStream"; }); } } Loading
packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java +145 −77 Original line number Diff line number Diff line Loading @@ -41,6 +41,7 @@ import com.google.common.util.concurrent.ListenableFuture; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.time.Duration; Loading Loading @@ -109,6 +110,39 @@ class ImageExporter { mQuality = quality; } /** * Stores the given Bitmap to a temp file. */ ListenableFuture<File> exportAsTempFile(Executor executor, Bitmap bitmap) { return CallbackToFutureAdapter.getFuture( (completer) -> { executor.execute(() -> { File cachePath; try { cachePath = File.createTempFile("long_screenshot_cache_", ".tmp"); try (FileOutputStream stream = new FileOutputStream(cachePath)) { bitmap.compress(mCompressFormat, mQuality, stream); } catch (IOException e) { if (cachePath.exists()) { //noinspection ResultOfMethodCallIgnored cachePath.delete(); cachePath = null; } completer.setException(e); } if (cachePath != null) { completer.set(cachePath); } } catch (IOException e) { // Failed to create a new file completer.setException(e); } }); return "Bitmap#compress"; } ); } /** * Export the image using the given executor. * Loading @@ -122,7 +156,7 @@ class ImageExporter { } /** * Export the image using the given executor. * Export the image to MediaStore and publish. * * @param executor the thread for execution * @param bitmap the bitmap to export Loading @@ -131,8 +165,10 @@ class ImageExporter { */ ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap, ZonedDateTime captureTime) { final Task task = new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat, mQuality); final Task task = new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat, mQuality, /* publish */ true); return CallbackToFutureAdapter.getFuture( (completer) -> { executor.execute(() -> { Loading @@ -147,12 +183,36 @@ class ImageExporter { ); } /** * Delete the entry. * * @param executor the thread for execution * @param uri the uri of the image to publish * * @return a listenable future result */ ListenableFuture<Result> delete(Executor executor, Uri uri) { return CallbackToFutureAdapter.getFuture((completer) -> { executor.execute(() -> { mResolver.delete(uri, null); Result result = new Result(); result.uri = uri; result.deleted = true; completer.set(result); }); return "ContentResolver#delete"; }); } static class Result { Uri uri; UUID requestId; String fileName; long timestamp; Uri uri; CompressFormat format; boolean published; boolean deleted; } private static class Task { Loading @@ -163,9 +223,10 @@ class ImageExporter { private final CompressFormat mFormat; private final int mQuality; private final String mFileName; private final boolean mPublish; Task(ContentResolver resolver, UUID requestId, Bitmap bitmap, ZonedDateTime captureTime, CompressFormat format, int quality) { CompressFormat format, int quality, boolean publish) { mResolver = resolver; mRequestId = requestId; mBitmap = bitmap; Loading @@ -173,6 +234,7 @@ class ImageExporter { mFormat = format; mQuality = quality; mFileName = createFilename(mCaptureTime, mFormat); mPublish = publish; } public Result execute() throws ImageExportException, InterruptedException { Loading @@ -186,16 +248,21 @@ class ImageExporter { start = Instant.now(); } uri = createEntry(mFormat, mCaptureTime, mFileName); uri = createEntry(mResolver, mFormat, mCaptureTime, mFileName); throwIfInterrupted(); writeImage(mBitmap, mFormat, mQuality, uri); writeImage(mResolver, mBitmap, mFormat, mQuality, uri); throwIfInterrupted(); writeExif(uri, mRequestId, mBitmap.getWidth(), mBitmap.getHeight(), mCaptureTime); int width = mBitmap.getWidth(); int height = mBitmap.getHeight(); writeExif(mResolver, uri, mRequestId, width, height, mCaptureTime); throwIfInterrupted(); publishEntry(uri); if (mPublish) { publishEntry(mResolver, uri); result.published = true; } result.timestamp = mCaptureTime.toInstant().toEpochMilli(); result.requestId = mRequestId; Loading @@ -218,13 +285,19 @@ class ImageExporter { return result; } Uri createEntry(CompressFormat format, ZonedDateTime time, String fileName) throws ImageExportException { @Override public String toString() { return "export [" + mBitmap + "] to [" + mFormat + "] at quality " + mQuality; } } private static Uri createEntry(ContentResolver resolver, CompressFormat format, ZonedDateTime time, String fileName) throws ImageExportException { Trace.beginSection("ImageExporter_createEntry"); try { final ContentValues values = createMetadata(time, format, fileName); Uri uri = mResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); if (uri == null) { throw new ImageExportException(RESOLVER_INSERT_RETURNED_NULL); } Loading @@ -234,10 +307,10 @@ class ImageExporter { } } void writeImage(Bitmap bitmap, CompressFormat format, int quality, Uri contentUri) throws ImageExportException { private static void writeImage(ContentResolver resolver, Bitmap bitmap, CompressFormat format, int quality, Uri contentUri) throws ImageExportException { Trace.beginSection("ImageExporter_writeImage"); try (OutputStream out = mResolver.openOutputStream(contentUri)) { try (OutputStream out = resolver.openOutputStream(contentUri)) { long start = SystemClock.elapsedRealtime(); if (!bitmap.compress(format, quality, out)) { throw new ImageExportException(IMAGE_COMPRESS_RETURNED_FALSE); Loading @@ -252,12 +325,12 @@ class ImageExporter { } } void writeExif(Uri uri, UUID requestId, int width, int height, ZonedDateTime captureTime) throws ImageExportException { private static void writeExif(ContentResolver resolver, Uri uri, UUID requestId, int width, int height, ZonedDateTime captureTime) throws ImageExportException { Trace.beginSection("ImageExporter_writeExif"); ParcelFileDescriptor pfd = null; try { pfd = mResolver.openFile(uri, "rw", null); pfd = resolver.openFile(uri, "rw", null); if (pfd == null) { throw new ImageExportException(RESOLVER_OPEN_FILE_RETURNED_NULL); } Loading @@ -282,13 +355,14 @@ class ImageExporter { } } void publishEntry(Uri uri) throws ImageExportException { private static void publishEntry(ContentResolver resolver, Uri uri) throws ImageExportException { Trace.beginSection("ImageExporter_publishEntry"); try { ContentValues values = new ContentValues(); values.put(MediaStore.MediaColumns.IS_PENDING, 0); values.putNull(MediaStore.MediaColumns.DATE_EXPIRES); final int rowsUpdated = mResolver.update(uri, values, /* extras */ null); final int rowsUpdated = resolver.update(uri, values, /* extras */ null); if (rowsUpdated < 1) { throw new ImageExportException(RESOLVER_UPDATE_ZERO_ROWS); } Loading @@ -297,12 +371,6 @@ class ImageExporter { } } @Override public String toString() { return "compress [" + mBitmap + "] to [" + mFormat + "] at quality " + mQuality; } } @VisibleForTesting static String createFilename(ZonedDateTime time, CompressFormat format) { return String.format(FILENAME_PATTERN, time, fileExtension(format)); Loading
packages/SystemUI/src/com/android/systemui/screenshot/ImageLoader.java 0 → 100644 +94 −0 Original line number Diff line number Diff line /* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.screenshot; import android.annotation.Nullable; import android.content.ContentResolver; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.ParcelFileDescriptor; import androidx.concurrent.futures.CallbackToFutureAdapter; import com.google.common.util.concurrent.ListenableFuture; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import javax.inject.Inject; /** Loads images. */ public class ImageLoader { private final ContentResolver mResolver; static class Result { @Nullable Uri uri; @Nullable File fileName; @Nullable Bitmap bitmap; } @Inject ImageLoader(ContentResolver resolver) { mResolver = resolver; } /** * Loads an image via URI from ContentResolver. * * @param uri the identifier of the image to load * @return a listenable future result */ ListenableFuture<Result> load(Uri uri) { return CallbackToFutureAdapter.getFuture(completer -> { Result result = new Result(); try (InputStream in = mResolver.openInputStream(uri)) { result.uri = uri; result.bitmap = BitmapFactory.decodeStream(in); completer.set(result); } catch (IOException e) { completer.setException(e); } return "BitmapFactory#decodeStream"; }); } /** * Loads an image by physical filesystem name. The current user must have filesystem * permissions to read this file/path. * * @param file the system file path of the image to load * @return a listenable future result */ ListenableFuture<Result> load(File file) { return CallbackToFutureAdapter.getFuture(completer -> { try (InputStream in = new BufferedInputStream(new FileInputStream(file))) { Result result = new Result(); result.fileName = file; result.bitmap = BitmapFactory.decodeStream(in); completer.set(result); } catch (IOException e) { completer.setException(e); } return "BitmapFactory#decodeStream"; }); } }