Merge "Image state restoration for LongScreenshotActivity" into sc-dev

This commit is contained in:
Matt Casey
2021-03-01 19:26:42 +00:00
committed by Android (Google) Code Review
5 changed files with 185 additions and 73 deletions

View File

@@ -338,6 +338,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"

View File

@@ -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.
*/

View File

@@ -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));
}
});
}
}

View File

@@ -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();
}

View File

@@ -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;