Merge "Implements initial long screenshot preview UI" into sc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
e07bbc9889
84
packages/SystemUI/res/layout/long_screenshot.xml
Normal file
84
packages/SystemUI/res/layout/long_screenshot.xml
Normal 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>
|
||||
|
||||
@@ -19,6 +19,7 @@ import static android.graphics.ColorSpace.Named.SRGB;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.ColorSpace;
|
||||
import android.graphics.RecordingCanvas;
|
||||
@@ -50,14 +51,13 @@ class ImageTile implements AutoCloseable {
|
||||
* @param image an image containing a hardware buffer
|
||||
* @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");
|
||||
mLocation = location;
|
||||
|
||||
mLocation = requireNonNull(location);
|
||||
requireNonNull(mImage.getHardwareBuffer(), "image must be a hardware image");
|
||||
}
|
||||
|
||||
RenderNode getDisplayList() {
|
||||
synchronized RenderNode getDisplayList() {
|
||||
if (mNode == null) {
|
||||
mNode = new RenderNode("Tile{" + Integer.toHexString(mImage.hashCode()) + "}");
|
||||
}
|
||||
@@ -69,7 +69,6 @@ class ImageTile implements AutoCloseable {
|
||||
mNode.setPosition(0, 0, w, h);
|
||||
|
||||
RecordingCanvas canvas = mNode.beginRecording(w, h);
|
||||
Rect rect = new Rect(0, 0, w, h);
|
||||
canvas.save();
|
||||
canvas.clipRect(0, 0, mLocation.right, mLocation.bottom);
|
||||
canvas.drawBitmap(Bitmap.wrapHardwareBuffer(mImage.getHardwareBuffer(), COLOR_SPACE),
|
||||
@@ -100,9 +99,11 @@ class ImageTile implements AutoCloseable {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
public synchronized void close() {
|
||||
mImage.close();
|
||||
mNode.discardDisplayList();
|
||||
if (mNode != null) {
|
||||
mNode.discardDisplayList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -603,7 +603,18 @@ public class ScreenshotController {
|
||||
cancelTimeout();
|
||||
ScrollCaptureController controller = new ScrollCaptureController(mContext, connection,
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,16 +16,24 @@
|
||||
|
||||
package com.android.systemui.screenshot;
|
||||
|
||||
import android.annotation.IdRes;
|
||||
import android.annotation.UiThread;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.UserHandle;
|
||||
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.Session;
|
||||
import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
@@ -38,11 +46,9 @@ import java.util.function.Consumer;
|
||||
/**
|
||||
* Interaction controller between the UI and ScrollCaptureClient.
|
||||
*/
|
||||
public class ScrollCaptureController {
|
||||
public class ScrollCaptureController implements OnComputeInternalInsetsListener {
|
||||
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_HEIGHT = 12000;
|
||||
|
||||
@@ -53,9 +59,19 @@ public class ScrollCaptureController {
|
||||
private final Executor mBgExecutor;
|
||||
private final ImageExporter mImageExporter;
|
||||
private final ImageTileSet mImageTileSet;
|
||||
private final LayoutInflater mLayoutInflater;
|
||||
|
||||
private ZonedDateTime mCaptureTime;
|
||||
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,
|
||||
Executor bgExecutor, ImageExporter exporter) {
|
||||
@@ -65,20 +81,126 @@ public class ScrollCaptureController {
|
||||
mBgExecutor = bgExecutor;
|
||||
mImageExporter = exporter;
|
||||
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!
|
||||
*
|
||||
* @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();
|
||||
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 =
|
||||
new Consumer<ScrollCaptureClient.CaptureResult>() {
|
||||
|
||||
@@ -91,17 +213,17 @@ public class ScrollCaptureController {
|
||||
|
||||
boolean emptyFrame = result.captured.height() == 0;
|
||||
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
|
||||
|| mTop + session.getTileHeight() > MAX_HEIGHT) {
|
||||
if (!mImageTileSet.isEmpty()) {
|
||||
exportToFile(mImageTileSet.toBitmap(), session, onDismiss);
|
||||
mImageTileSet.clear();
|
||||
} else {
|
||||
session.end(onDismiss);
|
||||
}
|
||||
|
||||
mUiExecutor.execute(() -> afterCaptureComplete(session));
|
||||
return;
|
||||
}
|
||||
mTop += result.captured.height();
|
||||
@@ -113,25 +235,24 @@ public class ScrollCaptureController {
|
||||
session.requestTile(0, consumer);
|
||||
};
|
||||
|
||||
void exportToFile(Bitmap bitmap, Session session, Runnable afterEnd) {
|
||||
mImageExporter.setFormat(Bitmap.CompressFormat.PNG);
|
||||
mImageExporter.setQuality(6);
|
||||
ListenableFuture<ImageExporter.Result> future =
|
||||
mImageExporter.export(mBgExecutor, mRequestId, bitmap, mCaptureTime);
|
||||
future.addListener(() -> {
|
||||
try {
|
||||
ImageExporter.Result result = future.get();
|
||||
launchViewer(result.uri);
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
Toast.makeText(mContext, "Failed to write image", Toast.LENGTH_SHORT).show();
|
||||
Log.e(TAG, "Error storing screenshot to media store", e.getCause());
|
||||
@UiThread
|
||||
void afterCaptureComplete(Session session) {
|
||||
Log.d(TAG, "afterCaptureComplete");
|
||||
|
||||
if (mImageTileSet.isEmpty()) {
|
||||
session.end(mCallback::onFinish);
|
||||
} else {
|
||||
mExportFuture = mImageExporter.export(
|
||||
mBgExecutor, mRequestId, mImageTileSet.toBitmap(), mCaptureTime);
|
||||
// The user chose an action already, link it to the result
|
||||
if (mPendingAction != null) {
|
||||
mExportFuture.addListener(mPendingAction, mUiExecutor);
|
||||
}
|
||||
session.end(afterEnd); // end session, close connection, afterEnd.run()
|
||||
}, mUiExecutor);
|
||||
}
|
||||
}
|
||||
|
||||
void launchViewer(Uri uri) {
|
||||
Intent editIntent = new Intent(Intent.ACTION_VIEW);
|
||||
void sendIntent(String action, Uri uri) {
|
||||
Intent editIntent = new Intent(action);
|
||||
editIntent.setType("image/png");
|
||||
editIntent.setData(uri);
|
||||
editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
Reference in New Issue
Block a user