Image state restoration for LongScreenshotActivity
- When long screenshot is acquired, save image to a temporary file. - When restoring state, read in that bitmap. - Moved bitmap export code from ScrollCaptureController to LongScreenshotActivity because it needs to be able to export any drawable regardless of source. - Currently deleting the temporary file onDestroy. Bug: 181087631 Test: Enable "don't keep activities" debug setting. Take long screenshot, go to overview and return. Rotate UI. Verify that image restores along with crop bounds and export still works properly. Change-Id: I892ad78025c818c8c65d16f2844daa955cc0b005
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -139,6 +139,25 @@ public class CropView extends View {
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user