Merge "Implements initial long screenshot preview UI" into sc-dev

This commit is contained in:
TreeHugger Robot
2021-01-28 22:17:59 +00:00
committed by Android (Google) Code Review
4 changed files with 257 additions and 40 deletions

View File

@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:background="?android:colorBackgroundFloating"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/preview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:layout_marginHorizontal="48dp"
android:adjustViewBounds="true"
app:layout_constrainedHeight="true"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:background="?android:colorBackground"
tools:minHeight="100dp"
tools:minWidth="100dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.9" />
<Button
android:id="@+id/close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="Close"
app:layout_constraintEnd_toStartOf="@+id/edit"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/guideline" />
<Button
android:id="@+id/edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="Edit"
app:layout_constraintEnd_toStartOf="@+id/share"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/close"
app:layout_constraintTop_toTopOf="@+id/guideline" />
<Button
android:id="@+id/share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="Share"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/edit"
app:layout_constraintTop_toTopOf="@+id/guideline" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -19,6 +19,7 @@ import static android.graphics.ColorSpace.Named.SRGB;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import android.annotation.NonNull;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.ColorSpace; import android.graphics.ColorSpace;
import android.graphics.RecordingCanvas; import android.graphics.RecordingCanvas;
@@ -50,14 +51,13 @@ class ImageTile implements AutoCloseable {
* @param image an image containing a hardware buffer * @param image an image containing a hardware buffer
* @param location the captured area represented by image tile (virtual coordinates) * @param location the captured area represented by image tile (virtual coordinates)
*/ */
ImageTile(Image image, Rect location) { ImageTile(@NonNull Image image, @NonNull Rect location) {
mImage = requireNonNull(image, "image"); mImage = requireNonNull(image, "image");
mLocation = location; mLocation = requireNonNull(location);
requireNonNull(mImage.getHardwareBuffer(), "image must be a hardware image"); requireNonNull(mImage.getHardwareBuffer(), "image must be a hardware image");
} }
RenderNode getDisplayList() { synchronized RenderNode getDisplayList() {
if (mNode == null) { if (mNode == null) {
mNode = new RenderNode("Tile{" + Integer.toHexString(mImage.hashCode()) + "}"); mNode = new RenderNode("Tile{" + Integer.toHexString(mImage.hashCode()) + "}");
} }
@@ -69,7 +69,6 @@ class ImageTile implements AutoCloseable {
mNode.setPosition(0, 0, w, h); mNode.setPosition(0, 0, w, h);
RecordingCanvas canvas = mNode.beginRecording(w, h); RecordingCanvas canvas = mNode.beginRecording(w, h);
Rect rect = new Rect(0, 0, w, h);
canvas.save(); canvas.save();
canvas.clipRect(0, 0, mLocation.right, mLocation.bottom); canvas.clipRect(0, 0, mLocation.right, mLocation.bottom);
canvas.drawBitmap(Bitmap.wrapHardwareBuffer(mImage.getHardwareBuffer(), COLOR_SPACE), canvas.drawBitmap(Bitmap.wrapHardwareBuffer(mImage.getHardwareBuffer(), COLOR_SPACE),
@@ -100,9 +99,11 @@ class ImageTile implements AutoCloseable {
} }
@Override @Override
public void close() { public synchronized void close() {
mImage.close(); mImage.close();
mNode.discardDisplayList(); if (mNode != null) {
mNode.discardDisplayList();
}
} }
@Override @Override

View File

@@ -603,7 +603,18 @@ public class ScreenshotController {
cancelTimeout(); cancelTimeout();
ScrollCaptureController controller = new ScrollCaptureController(mContext, connection, ScrollCaptureController controller = new ScrollCaptureController(mContext, connection,
mMainExecutor, mBgExecutor, mImageExporter); mMainExecutor, mBgExecutor, mImageExporter);
controller.start(/* onDismiss */ () -> dismissScreenshot(false)); controller.attach(mWindow);
controller.start(new TakeScreenshotService.RequestCallback() {
@Override
public void reportError() {
}
@Override
public void onFinish() {
Log.d(TAG, "onFinish from ScrollCaptureController");
finishDismiss();
}
});
} }
/** /**

View File

@@ -16,16 +16,24 @@
package com.android.systemui.screenshot; package com.android.systemui.screenshot;
import android.annotation.IdRes;
import android.annotation.UiThread;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.os.UserHandle; import android.os.UserHandle;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewTreeObserver.InternalInsetsInfo;
import android.view.ViewTreeObserver.OnComputeInternalInsetsListener;
import android.view.Window;
import android.widget.ImageView;
import com.android.systemui.R;
import com.android.systemui.screenshot.ScrollCaptureClient.Connection; import com.android.systemui.screenshot.ScrollCaptureClient.Connection;
import com.android.systemui.screenshot.ScrollCaptureClient.Session; import com.android.systemui.screenshot.ScrollCaptureClient.Session;
import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@@ -38,11 +46,9 @@ import java.util.function.Consumer;
/** /**
* Interaction controller between the UI and ScrollCaptureClient. * Interaction controller between the UI and ScrollCaptureClient.
*/ */
public class ScrollCaptureController { public class ScrollCaptureController implements OnComputeInternalInsetsListener {
private static final String TAG = "ScrollCaptureController"; private static final String TAG = "ScrollCaptureController";
private static final boolean USE_TILED_IMAGE = false;
public static final int MAX_PAGES = 5; public static final int MAX_PAGES = 5;
public static final int MAX_HEIGHT = 12000; public static final int MAX_HEIGHT = 12000;
@@ -53,9 +59,19 @@ public class ScrollCaptureController {
private final Executor mBgExecutor; private final Executor mBgExecutor;
private final ImageExporter mImageExporter; private final ImageExporter mImageExporter;
private final ImageTileSet mImageTileSet; private final ImageTileSet mImageTileSet;
private final LayoutInflater mLayoutInflater;
private ZonedDateTime mCaptureTime; private ZonedDateTime mCaptureTime;
private UUID mRequestId; private UUID mRequestId;
private RequestCallback mCallback;
private Window mWindow;
private ImageView mPreview;
private View mClose;
private View mEdit;
private View mShare;
private ListenableFuture<ImageExporter.Result> mExportFuture;
private Runnable mPendingAction;
public ScrollCaptureController(Context context, Connection connection, Executor uiExecutor, public ScrollCaptureController(Context context, Connection connection, Executor uiExecutor,
Executor bgExecutor, ImageExporter exporter) { Executor bgExecutor, ImageExporter exporter) {
@@ -65,20 +81,126 @@ public class ScrollCaptureController {
mBgExecutor = bgExecutor; mBgExecutor = bgExecutor;
mImageExporter = exporter; mImageExporter = exporter;
mImageTileSet = new ImageTileSet(); mImageTileSet = new ImageTileSet();
mLayoutInflater = mContext.getSystemService(LayoutInflater.class);
}
/**
* @param window the window to display the preview
*/
public void attach(Window window) {
mWindow = window;
} }
/** /**
* Run scroll capture! * Run scroll capture!
* *
* @param after action to take after the flow is complete * @param callback request callback to report back to the service
*/ */
public void start(final Runnable after) { public void start(RequestCallback callback) {
mCaptureTime = ZonedDateTime.now(); mCaptureTime = ZonedDateTime.now();
mRequestId = UUID.randomUUID(); mRequestId = UUID.randomUUID();
mConnection.start((session) -> startCapture(session, after)); mCallback = callback;
setContentView(R.layout.long_screenshot);
mWindow.getDecorView().getViewTreeObserver()
.addOnComputeInternalInsetsListener(this);
mPreview = findViewById(R.id.preview);
mClose = findViewById(R.id.close);
mEdit = findViewById(R.id.edit);
mShare = findViewById(R.id.share);
mClose.setOnClickListener(this::onClicked);
mEdit.setOnClickListener(this::onClicked);
mShare.setOnClickListener(this::onClicked);
mPreview.setImageDrawable(mImageTileSet.getDrawable());
mConnection.start(this::startCapture);
} }
private void startCapture(Session session, final Runnable onDismiss) {
/** Ensure the entire window is touchable */
public void onComputeInternalInsets(InternalInsetsInfo inoutInfo) {
inoutInfo.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
}
void disableButtons() {
mClose.setEnabled(false);
mEdit.setEnabled(false);
mShare.setEnabled(false);
}
private void onClicked(View v) {
Log.d(TAG, "button clicked!");
int id = v.getId();
if (id == R.id.close) {
v.setPressed(true);
disableButtons();
finish();
} else if (id == R.id.edit) {
v.setPressed(true);
disableButtons();
edit();
} else if (id == R.id.share) {
v.setPressed(true);
disableButtons();
share();
}
}
private void finish() {
if (mExportFuture == null) {
doFinish();
} else {
mExportFuture.addListener(this::doFinish, mUiExecutor);
}
}
private void doFinish() {
mPreview.setImageDrawable(null);
mImageTileSet.clear();
mCallback.onFinish();
mWindow.getDecorView().getViewTreeObserver()
.removeOnComputeInternalInsetsListener(this);
}
private void edit() {
sendIntentWhenReady(Intent.ACTION_EDIT);
}
private void share() {
sendIntentWhenReady(Intent.ACTION_SEND);
}
void sendIntentWhenReady(String action) {
if (mExportFuture != null) {
mExportFuture.addListener(() -> {
try {
ImageExporter.Result result = mExportFuture.get();
sendIntent(action, result.uri);
mCallback.onFinish();
} catch (InterruptedException | ExecutionException e) {
Log.e(TAG, "failed to export", e);
mCallback.onFinish();
}
}, mUiExecutor);
} else {
mPendingAction = this::edit;
}
}
private void setContentView(@IdRes int id) {
mWindow.setContentView(id);
}
<T extends View> T findViewById(@IdRes int res) {
return mWindow.findViewById(res);
}
private void startCapture(Session session) {
Log.d(TAG, "startCapture");
Consumer<ScrollCaptureClient.CaptureResult> consumer = Consumer<ScrollCaptureClient.CaptureResult> consumer =
new Consumer<ScrollCaptureClient.CaptureResult>() { new Consumer<ScrollCaptureClient.CaptureResult>() {
@@ -91,17 +213,17 @@ public class ScrollCaptureController {
boolean emptyFrame = result.captured.height() == 0; boolean emptyFrame = result.captured.height() == 0;
if (!emptyFrame) { if (!emptyFrame) {
mImageTileSet.addTile(new ImageTile(result.image, result.captured)); ImageTile tile = new ImageTile(result.image, result.captured);
Log.d(TAG, "Adding tile: " + tile);
mImageTileSet.addTile(tile);
Log.d(TAG, "New dimens: w=" + mImageTileSet.getWidth() + ", "
+ "h=" + mImageTileSet.getHeight());
} }
if (emptyFrame || mFrameCount >= MAX_PAGES if (emptyFrame || mFrameCount >= MAX_PAGES
|| mTop + session.getTileHeight() > MAX_HEIGHT) { || mTop + session.getTileHeight() > MAX_HEIGHT) {
if (!mImageTileSet.isEmpty()) {
exportToFile(mImageTileSet.toBitmap(), session, onDismiss); mUiExecutor.execute(() -> afterCaptureComplete(session));
mImageTileSet.clear();
} else {
session.end(onDismiss);
}
return; return;
} }
mTop += result.captured.height(); mTop += result.captured.height();
@@ -113,25 +235,24 @@ public class ScrollCaptureController {
session.requestTile(0, consumer); session.requestTile(0, consumer);
}; };
void exportToFile(Bitmap bitmap, Session session, Runnable afterEnd) { @UiThread
mImageExporter.setFormat(Bitmap.CompressFormat.PNG); void afterCaptureComplete(Session session) {
mImageExporter.setQuality(6); Log.d(TAG, "afterCaptureComplete");
ListenableFuture<ImageExporter.Result> future =
mImageExporter.export(mBgExecutor, mRequestId, bitmap, mCaptureTime); if (mImageTileSet.isEmpty()) {
future.addListener(() -> { session.end(mCallback::onFinish);
try { } else {
ImageExporter.Result result = future.get(); mExportFuture = mImageExporter.export(
launchViewer(result.uri); mBgExecutor, mRequestId, mImageTileSet.toBitmap(), mCaptureTime);
} catch (InterruptedException | ExecutionException e) { // The user chose an action already, link it to the result
Toast.makeText(mContext, "Failed to write image", Toast.LENGTH_SHORT).show(); if (mPendingAction != null) {
Log.e(TAG, "Error storing screenshot to media store", e.getCause()); mExportFuture.addListener(mPendingAction, mUiExecutor);
} }
session.end(afterEnd); // end session, close connection, afterEnd.run() }
}, mUiExecutor);
} }
void launchViewer(Uri uri) { void sendIntent(String action, Uri uri) {
Intent editIntent = new Intent(Intent.ACTION_VIEW); Intent editIntent = new Intent(action);
editIntent.setType("image/png"); editIntent.setType("image/png");
editIntent.setData(uri); editIntent.setData(uri);
editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);