diff options
5 files changed, 185 insertions, 73 deletions
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index fbe58c505662..58e129f4af7c 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -337,6 +337,7 @@ <activity android:name=".screenshot.LongScreenshotActivity" android:theme="@android:style/Theme.DeviceDefault.NoActionBar" android:process=":screenshot" + android:exported="false" android:finishOnTaskLaunch="true" /> <activity android:name=".screenrecord.ScreenRecordDialog" diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java b/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java index 53d9f1c08e6f..5b55864eed8a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java @@ -140,6 +140,25 @@ public class CropView extends View { } /** + * Set the given boundary to the given value without animation. + */ + public void setBoundaryTo(CropBoundary boundary, float value) { + switch (boundary) { + case TOP: + mTopCrop = value; + break; + case BOTTOM: + mBottomCrop = value; + break; + case NONE: + Log.w(TAG, "No boundary selected for animation"); + break; + } + + invalidate(); + } + + /** * Animate the given boundary to the given value. */ public void animateBoundaryTo(CropBoundary boundary, float value) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java index 89efda98a5b6..5a13ea55222d 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java @@ -20,8 +20,17 @@ import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.HardwareRenderer; +import android.graphics.RecordingCanvas; +import android.graphics.Rect; +import android.graphics.RenderNode; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; +import android.os.SystemClock; import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; @@ -33,6 +42,14 @@ import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; +import com.google.common.util.concurrent.ListenableFuture; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.time.ZonedDateTime; +import java.util.UUID; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import javax.inject.Inject; @@ -44,9 +61,21 @@ import javax.inject.Inject; public class LongScreenshotActivity extends Activity { private static final String TAG = "LongScreenshotActivity"; + private static final String IMAGE_PATH_KEY = "saved-image"; + private static final String TOP_BOUNDARY_KEY = "top-boundary"; + private static final String BOTTOM_BOUNDARY_KEY = "bottom-boundary"; + private final UiEventLogger mUiEventLogger; private final ScrollCaptureController mScrollCaptureController; private final ScrollCaptureClient.Connection mConnection; + private final Executor mUiExecutor; + private final Executor mBackgroundExecutor; + private final ImageExporter mImageExporter; + + private String mSavedImagePath; + // If true, the activity is re-loading an image from storage, which should either succeed and + // populate the UI or fail and finish the activity. + private boolean mRestoringInstance; private ImageView mPreview; private View mSave; @@ -69,6 +98,9 @@ public class LongScreenshotActivity extends Activity { @Background Executor bgExecutor, Context context) { mUiEventLogger = uiEventLogger; + mUiExecutor = mainExecutor; + mBackgroundExecutor = bgExecutor; + mImageExporter = imageExporter; mScrollCaptureController = new ScrollCaptureController(context, mainExecutor, bgExecutor, imageExporter); @@ -95,12 +127,42 @@ public class LongScreenshotActivity extends Activity { mCancel.setOnClickListener(this::onClicked); mEdit.setOnClickListener(this::onClicked); mShare.setOnClickListener(this::onClicked); + + if (savedInstanceState != null) { + String imagePath = savedInstanceState.getString(IMAGE_PATH_KEY); + if (!TextUtils.isEmpty(imagePath)) { + mRestoringInstance = true; + mBackgroundExecutor.execute(() -> { + Bitmap bitmap = BitmapFactory.decodeFile(imagePath); + if (bitmap == null) { + Log.e(TAG, "Failed to read bitmap from " + imagePath); + finishAndRemoveTask(); + } else { + runOnUiThread(() -> { + BitmapDrawable drawable = new BitmapDrawable(getResources(), bitmap); + mPreview.setImageDrawable(drawable); + mMagnifierView.setDrawable(drawable, bitmap.getWidth(), + bitmap.getHeight()); + + mCropView.setBoundaryTo(CropView.CropBoundary.TOP, + savedInstanceState.getFloat(TOP_BOUNDARY_KEY, 0f)); + mCropView.setBoundaryTo(CropView.CropBoundary.BOTTOM, + savedInstanceState.getFloat(BOTTOM_BOUNDARY_KEY, 1f)); + mRestoringInstance = false; + // Reuse the same path for subsequent restoration. + mSavedImagePath = imagePath; + Log.d(TAG, "Loaded bitmap from " + imagePath); + }); + } + }); + } + } } @Override public void onStart() { super.onStart(); - if (mPreview.getDrawable() == null) { + if (mPreview.getDrawable() == null && !mRestoringInstance) { if (mConnection == null) { Log.e(TAG, "Failed to get scroll capture connection, bailing out"); finishAndRemoveTask(); @@ -110,6 +172,24 @@ public class LongScreenshotActivity extends Activity { } } + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString(IMAGE_PATH_KEY, mSavedImagePath); + outState.putFloat(TOP_BOUNDARY_KEY, mCropView.getTopBoundary()); + outState.putFloat(BOTTOM_BOUNDARY_KEY, mCropView.getBottomBoundary()); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (isFinishing() && !TextUtils.isEmpty(mSavedImagePath)) { + Log.d(TAG, "Deleting " + mSavedImagePath); + File file = new File(mSavedImagePath); + file.delete(); + } + } + private void setButtonsEnabled(boolean enabled) { mSave.setEnabled(enabled); mCancel.setEnabled(enabled); @@ -161,50 +241,89 @@ public class LongScreenshotActivity extends Activity { } private void startExport(PendingAction action) { - mScrollCaptureController.startExport(mCropView.getTopBoundary(), - mCropView.getBottomBoundary(), new ScrollCaptureController.ExportCallback() { - @Override - public void onError() { - Log.e(TAG, "Error exporting image data."); - setButtonsEnabled(true); - } + Drawable drawable = mPreview.getDrawable(); - @Override - public void onExportComplete(Uri outputUri) { - setButtonsEnabled(true); - switch (action) { - case EDIT: - doEdit(outputUri); - break; - case SHARE: - doShare(outputUri); - break; - case SAVE: - // Nothing more to do - finishAndRemoveTask(); - break; - } - } - }); + Rect croppedPortion = new Rect( + 0, + (int) (drawable.getIntrinsicHeight() * mCropView.getTopBoundary()), + drawable.getIntrinsicWidth(), + (int) (drawable.getIntrinsicHeight() * mCropView.getBottomBoundary())); + ListenableFuture<ImageExporter.Result> exportFuture = mImageExporter.export( + mBackgroundExecutor, UUID.randomUUID(), getBitmap(croppedPortion, drawable), + ZonedDateTime.now()); + exportFuture.addListener(() -> { + try { + ImageExporter.Result result = exportFuture.get(); + setButtonsEnabled(true); + switch (action) { + case EDIT: + doEdit(result.uri); + break; + case SHARE: + doShare(result.uri); + break; + case SAVE: + // Nothing more to do + finishAndRemoveTask(); + break; + } + } catch (InterruptedException | ExecutionException e) { + Log.e(TAG, "failed to export", e); + setButtonsEnabled(true); + } + }, mUiExecutor); + } + + private Bitmap getBitmap(Rect bounds, Drawable drawable) { + final RenderNode output = new RenderNode("Bitmap Export"); + output.setPosition(0, 0, bounds.width(), bounds.height()); + RecordingCanvas canvas = output.beginRecording(); + // Translating the canvas instead of setting drawable bounds since the drawable is still + // used in the preview. + canvas.translate(0, -bounds.top); + drawable.draw(canvas); + output.endRecording(); + return HardwareRenderer.createHardwareBitmap(output, bounds.width(), bounds.height()); + } + + private void saveCacheBitmap(ImageTileSet tileSet) { + long startTime = SystemClock.uptimeMillis(); + Bitmap bitmap = tileSet.toBitmap(); + // TODO(b/181562529) Remove this + mPreview.setImageDrawable(tileSet.getDrawable()); + try { + File file = File.createTempFile("long_screenshot", ".png", null); + FileOutputStream stream = new FileOutputStream(file); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); + stream.flush(); + stream.close(); + mSavedImagePath = file.getAbsolutePath(); + Log.d(TAG, "Saved to " + file.getAbsolutePath() + " in " + + (SystemClock.uptimeMillis() - startTime) + "ms"); + } catch (IOException e) { + Log.e(TAG, "Failed to save bitmap", e); + } } private void doCapture() { mScrollCaptureController.start(mConnection, new ScrollCaptureController.ScrollCaptureCallback() { - @Override - public void onError() { - Log.e(TAG, "Error!"); - finishAndRemoveTask(); - } + @Override + public void onError() { + Log.e(TAG, "Error capturing long screenshot!"); + finishAndRemoveTask(); + } - @Override - public void onComplete(ImageTileSet imageTileSet) { - Log.i(TAG, "Got tiles " + imageTileSet.getWidth() + " x " - + imageTileSet.getHeight()); - mPreview.setImageDrawable(imageTileSet.getDrawable()); - mMagnifierView.setImageTileset(imageTileSet); - mCropView.animateBoundaryTo(CropView.CropBoundary.BOTTOM, 0.5f); - } - }); + @Override + public void onComplete(ImageTileSet imageTileSet) { + Log.i(TAG, "Got tiles " + imageTileSet.getWidth() + " x " + + imageTileSet.getHeight()); + mPreview.setImageDrawable(imageTileSet.getDrawable()); + mMagnifierView.setDrawable(imageTileSet.getDrawable(), + imageTileSet.getWidth(), imageTileSet.getHeight()); + mCropView.animateBoundaryTo(CropView.CropBoundary.BOTTOM, 0.5f); + mBackgroundExecutor.execute(() -> saveCacheBitmap(imageTileSet)); + } + }); } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/MagnifierView.java b/packages/SystemUI/src/com/android/systemui/screenshot/MagnifierView.java index f8f1d3ac9a5b..7a0ec4c520b2 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/MagnifierView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/MagnifierView.java @@ -16,6 +16,7 @@ package com.android.systemui.screenshot; +import android.annotation.NonNull; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; @@ -77,13 +78,12 @@ public class MagnifierView extends View implements CropView.CropInteractionListe mCheckerboardPaint.setColor(Color.GRAY); } - public void setImageTileset(ImageTileSet tiles) { - if (tiles != null) { - mDrawable = tiles.getDrawable(); - mDrawable.setBounds(0, 0, tiles.getWidth(), tiles.getHeight()); - } else { - mDrawable = null; - } + /** + * Set the drawable to be displayed by the magnifier. + */ + public void setDrawable(@NonNull Drawable drawable, int width, int height) { + mDrawable = drawable; + mDrawable.setBounds(0, 0, width, height); invalidate(); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java index 4a3ffa45ab81..bf65132166b6 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java @@ -18,7 +18,6 @@ package com.android.systemui.screenshot; import android.annotation.UiThread; import android.content.Context; -import android.graphics.Rect; import android.net.Uri; import android.provider.Settings; import android.util.Log; @@ -27,11 +26,8 @@ import com.android.systemui.screenshot.ScrollCaptureClient.CaptureResult; import com.android.systemui.screenshot.ScrollCaptureClient.Connection; 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; /** @@ -89,29 +85,6 @@ public class ScrollCaptureController { connection.start(this::startCapture, maxPages); } - /** - * @param topCrop [0,1) fraction of the top of the image to be cropped out. - * @param bottomCrop (0, 1] fraction to be cropped out, e.g. 0.7 will crop out the bottom 30%. - */ - public void startExport(float topCrop, float bottomCrop, ExportCallback callback) { - Rect croppedPortion = new Rect( - 0, - (int) (mImageTileSet.getHeight() * topCrop), - mImageTileSet.getWidth(), - (int) (mImageTileSet.getHeight() * bottomCrop)); - ListenableFuture<ImageExporter.Result> exportFuture = mImageExporter.export( - mBgExecutor, mRequestId, mImageTileSet.toBitmap(croppedPortion), mCaptureTime); - exportFuture.addListener(() -> { - try { - ImageExporter.Result result = exportFuture.get(); - callback.onExportComplete(result.uri); - } catch (InterruptedException | ExecutionException e) { - Log.e(TAG, "failed to export", e); - callback.onError(); - } - }, mUiExecutor); - } - private void onCaptureResult(CaptureResult result) { Log.d(TAG, "onCaptureResult: " + result); boolean emptyResult = result.captured.height() == 0; |