Merge "Splits up internal binder api for scroll capture" into sc-dev

This commit is contained in:
Mark Renouf
2021-03-16 18:21:37 +00:00
committed by Android (Google) Code Review
27 changed files with 911 additions and 709 deletions

View File

@@ -26,13 +26,6 @@ import android.view.Surface;
* {@hide}
*/
interface IScrollCaptureCallbacks {
/**
* Provides the result of WindowManagerService#requestScrollCapture
*
* @param response the response which describes the result
*/
oneway void onScrollCaptureResponse(in ScrollCaptureResponse response);
/**
* Called in reply to IScrollCaptureConnection#startCapture, when the remote end has confirmed
* the request and is ready to begin capturing images.

View File

@@ -18,6 +18,7 @@ package android.view;
import android.graphics.Rect;
import android.os.ICancellationSignal;
import android.view.IScrollCaptureCallbacks;
import android.view.Surface;
@@ -31,11 +32,12 @@ interface IScrollCaptureConnection {
/**
* Informs the target that it has been selected for scroll capture.
*
* @param surface a return channel for image buffers
* @param surface used to shuttle image buffers between processes
* @param callbacks a return channel for requests
*
* @return a cancallation signal which is used cancel the request
* @return a cancallation signal which is used cancel the start request
*/
ICancellationSignal startCapture(in Surface surface);
ICancellationSignal startCapture(in Surface surface, IScrollCaptureCallbacks callbacks);
/**
* Request the target capture an image within the provided rectangle.

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 The Android Open Source Project
* 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.
@@ -14,20 +14,22 @@
* limitations under the License.
*/
package com.android.systemui.screenshot;
package android.view;
import java.util.function.Consumer;
import android.graphics.Rect;
import android.view.ScrollCaptureResponse;
import android.view.Surface;
/** Accepts and retains the most recent value for verification */
class TestableConsumer<T> implements Consumer<T> {
T mValue;
@Override
public void accept(T t) {
mValue = t;
}
public T getValue() {
return mValue;
}
/**
* Asynchronous callback channel for the initial response to a scroll capture request.
*
* {@hide}
*/
interface IScrollCaptureResponseListener {
/**
* Provides the initial response to a scroll capture request.
*
* @param response the response which describes the result
*/
oneway void onScrollCaptureResponse(in ScrollCaptureResponse response);
}

View File

@@ -26,7 +26,7 @@ import android.view.DisplayCutout;
import android.view.DragEvent;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.IScrollCaptureCallbacks;
import android.view.IScrollCaptureResponseListener;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.window.ClientWindowFrames;
@@ -134,5 +134,5 @@ oneway interface IWindow {
*
* @param callbacks to receive responses
*/
void requestScrollCapture(in IScrollCaptureCallbacks callbacks);
void requestScrollCapture(in IScrollCaptureResponseListener callbacks);
}

View File

@@ -42,7 +42,7 @@ import android.view.IDisplayFoldListener;
import android.view.IDisplayWindowRotationController;
import android.view.IOnKeyguardExitResult;
import android.view.IPinnedTaskListener;
import android.view.IScrollCaptureCallbacks;
import android.view.IScrollCaptureResponseListener;
import android.view.RemoteAnimationAdapter;
import android.view.IRotationWatcher;
import android.view.ISystemGestureExclusionListener;
@@ -744,10 +744,10 @@ interface IWindowManager
* @param behindClient token for a window, used to filter the search to windows behind it, or
* {@code null} to accept a window at any zOrder
* @param taskId specifies the id of a task the result must belong to, or -1 to ignore task ids
* @param callbacks the object to receive replies
* @param listener the object to receive the response
*/
void requestScrollCapture(int displayId, IBinder behindClient, int taskId,
IScrollCaptureCallbacks callbacks);
IScrollCaptureResponseListener listener);
/**
* Holds the WM lock for the specified amount of milliseconds.

View File

@@ -32,6 +32,7 @@ import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
@@ -50,8 +51,9 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub {
private final Object mLock = new Object();
private final Rect mScrollBounds;
private final Point mPositionInWindow;
private final CloseGuard mCloseGuard;
private final Executor mUiThread;
private final CloseGuard mCloseGuard = new CloseGuard();
private ScrollCaptureCallback mLocal;
private IScrollCaptureCallbacks mRemote;
@@ -60,42 +62,38 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub {
private CancellationSignal mCancellation;
private volatile boolean mStarted;
private volatile boolean mConnected;
private volatile boolean mActive;
/**
* Constructs a ScrollCaptureConnection.
*
* @param uiThread an executor for the UI thread of the containing View
* @param selectedTarget the target the client is controlling
* @param remote the callbacks to reply to system requests
*
* @hide
*/
public ScrollCaptureConnection(
@NonNull Executor uiThread,
@NonNull ScrollCaptureTarget selectedTarget,
@NonNull IScrollCaptureCallbacks remote) {
@NonNull ScrollCaptureTarget selectedTarget) {
mUiThread = requireNonNull(uiThread, "<uiThread> must non-null");
requireNonNull(selectedTarget, "<selectedTarget> must non-null");
mRemote = requireNonNull(remote, "<callbacks> must non-null");
mScrollBounds = requireNonNull(Rect.copyOrNull(selectedTarget.getScrollBounds()),
"target.getScrollBounds() must be non-null to construct a client");
mLocal = selectedTarget.getCallback();
mPositionInWindow = new Point(selectedTarget.getPositionInWindow());
mCloseGuard = new CloseGuard();
mCloseGuard.open("close");
mConnected = true;
}
@BinderThread
@Override
public ICancellationSignal startCapture(Surface surface) throws RemoteException {
checkConnected();
public ICancellationSignal startCapture(@NonNull Surface surface,
@NonNull IScrollCaptureCallbacks remote) throws RemoteException {
mCloseGuard.open("close");
if (!surface.isValid()) {
throw new RemoteException(new IllegalArgumentException("surface must be valid"));
}
mRemote = requireNonNull(remote, "<callbacks> must non-null");
ICancellationSignal cancellation = CancellationSignal.createTransport();
mCancellation = CancellationSignal.fromTransport(cancellation);
@@ -110,7 +108,7 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub {
@UiThread
private void onStartCaptureCompleted() {
mStarted = true;
mActive = true;
try {
mRemote.onCaptureStarted();
} catch (RemoteException e) {
@@ -119,13 +117,11 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub {
}
}
@BinderThread
@Override
public ICancellationSignal requestImage(Rect requestRect) throws RemoteException {
Trace.beginSection("requestImage");
checkConnected();
checkStarted();
checkActive();
ICancellationSignal cancellation = CancellationSignal.createTransport();
mCancellation = CancellationSignal.fromTransport(cancellation);
@@ -152,8 +148,7 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub {
@BinderThread
@Override
public ICancellationSignal endCapture() throws RemoteException {
checkConnected();
checkStarted();
checkActive();
ICancellationSignal cancellation = CancellationSignal.createTransport();
mCancellation = CancellationSignal.fromTransport(cancellation);
@@ -167,64 +162,48 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub {
@UiThread
private void onEndCaptureCompleted() {
synchronized (mLock) {
mStarted = false;
try {
mActive = false;
try {
if (mRemote != null) {
mRemote.onCaptureEnded();
} catch (RemoteException e) {
Log.w(TAG, "Shutting down due to error: ", e);
close();
}
} catch (RemoteException e) {
Log.w(TAG, "Caught exception confirming capture end!", e);
} finally {
close();
}
}
@BinderThread
@Override
public void close() {
if (mStarted) {
Log.w(TAG, "close(): capture is still started?! Ending now.");
if (mActive) {
if (mCancellation != null) {
Log.w(TAG, "close(): cancelling pending operation.");
mCancellation.cancel();
mCancellation = null;
}
Log.w(TAG, "close(): capture session still active! Ending now.");
// -> UiThread
mUiThread.execute(() -> mLocal.onScrollCaptureEnd(() -> { /* ignore */ }));
mStarted = false;
mActive = false;
}
disconnect();
mActive = false;
mSession = null;
mRemote = null;
mLocal = null;
mCloseGuard.close();
Reference.reachabilityFence(this);
}
/**
* Shuts down this client and releases references to dependent objects. No attempt is made
* to notify the controller, use with caution!
*/
private void disconnect() {
@VisibleForTesting
public boolean isActive() {
return mActive;
}
private void checkActive() throws RemoteException {
synchronized (mLock) {
mSession = null;
mConnected = false;
mStarted = false;
mRemote = null;
mLocal = null;
mCloseGuard.close();
}
}
public boolean isConnected() {
return mConnected;
}
public boolean isStarted() {
return mStarted;
}
private synchronized void checkConnected() throws RemoteException {
synchronized (mLock) {
if (!mConnected) {
throw new RemoteException(new IllegalStateException("Not connected"));
}
}
}
private void checkStarted() throws RemoteException {
synchronized (mLock) {
if (!mStarted) {
if (!mActive) {
throw new RemoteException(new IllegalStateException("Not started!"));
}
}
@@ -233,24 +212,16 @@ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub {
/** @return a string representation of the state of this client */
public String toString() {
return "ScrollCaptureConnection{"
+ "connected=" + mConnected
+ ", started=" + mStarted
+ "active=" + mActive
+ ", session=" + mSession
+ ", remote=" + mRemote
+ ", local=" + mLocal
+ "}";
}
@VisibleForTesting
public CancellationSignal getCancellation() {
return mCancellation;
}
protected void finalize() throws Throwable {
try {
if (mCloseGuard != null) {
mCloseGuard.warnIfOpen();
}
mCloseGuard.warnIfOpen();
close();
} finally {
super.finalize();

View File

@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Rect;
import android.os.Parcelable;
import android.os.RemoteException;
import com.android.internal.util.DataClass;
@@ -57,11 +58,22 @@ public class ScrollCaptureResponse implements Parcelable {
@DataClass.PluralOf("message")
private ArrayList<String> mMessages = new ArrayList<>();
/** Whether a connection has been returned. */
/** Whether an active connection is present. */
public boolean isConnected() {
return mConnection != null;
return mConnection != null && mConnection.asBinder().isBinderAlive();
}
/** Closes a connection returned with this response. */
public void close() {
if (mConnection != null) {
try {
mConnection.close();
} catch (RemoteException e) {
// Ignore
}
mConnection = null;
}
}
@@ -367,10 +379,10 @@ public class ScrollCaptureResponse implements Parcelable {
}
@DataClass.Generated(
time = 1612282689462L,
time = 1614833185795L,
codegenVersion = "1.0.22",
sourceFile = "frameworks/base/core/java/android/view/ScrollCaptureResponse.java",
inputSignatures = "private @android.annotation.NonNull java.lang.String mDescription\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.MaySetToNull android.view.IScrollCaptureConnection mConnection\nprivate @android.annotation.Nullable android.graphics.Rect mWindowBounds\nprivate @android.annotation.Nullable android.graphics.Rect mBoundsInWindow\nprivate @android.annotation.Nullable java.lang.String mWindowTitle\nprivate @android.annotation.NonNull @com.android.internal.util.DataClass.PluralOf(\"message\") java.util.ArrayList<java.lang.String> mMessages\npublic boolean isConnected()\nclass ScrollCaptureResponse extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genGetters=true)")
inputSignatures = "private @android.annotation.NonNull java.lang.String mDescription\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.MaySetToNull android.view.IScrollCaptureConnection mConnection\nprivate @android.annotation.Nullable android.graphics.Rect mWindowBounds\nprivate @android.annotation.Nullable android.graphics.Rect mBoundsInWindow\nprivate @android.annotation.Nullable java.lang.String mWindowTitle\nprivate @android.annotation.NonNull @com.android.internal.util.DataClass.PluralOf(\"message\") java.util.ArrayList<java.lang.String> mMessages\npublic boolean isConnected()\npublic void close()\nclass ScrollCaptureResponse extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genGetters=true)")
@Deprecated
private void __metadata() {}

View File

@@ -5356,7 +5356,7 @@ public final class ViewRootImpl implements ViewParent,
updateLocationInParentDisplay(msg.arg1, msg.arg2);
} break;
case MSG_REQUEST_SCROLL_CAPTURE:
handleScrollCaptureRequest((IScrollCaptureCallbacks) msg.obj);
handleScrollCaptureRequest((IScrollCaptureResponseListener) msg.obj);
break;
}
}
@@ -9267,10 +9267,10 @@ public final class ViewRootImpl implements ViewParent,
/**
* Dispatches a scroll capture request to the view hierarchy on the ui thread.
*
* @param callbacks for replies
* @param listener for the response
*/
public void dispatchScrollCaptureRequest(@NonNull IScrollCaptureCallbacks callbacks) {
mHandler.obtainMessage(MSG_REQUEST_SCROLL_CAPTURE, callbacks).sendToTarget();
public void dispatchScrollCaptureRequest(@NonNull IScrollCaptureResponseListener listener) {
mHandler.obtainMessage(MSG_REQUEST_SCROLL_CAPTURE, listener).sendToTarget();
}
/**
@@ -9317,10 +9317,10 @@ public final class ViewRootImpl implements ViewParent,
* A call to {@link IScrollCaptureCallbacks#onScrollCaptureResponse(ScrollCaptureResponse)}
* will follow.
*
* @param callbacks to receive responses
* @param listener to receive responses
* @see ScrollCaptureTargetSelector
*/
public void handleScrollCaptureRequest(@NonNull IScrollCaptureCallbacks callbacks) {
public void handleScrollCaptureRequest(@NonNull IScrollCaptureResponseListener listener) {
ScrollCaptureSearchResults results =
new ScrollCaptureSearchResults(mContext.getMainExecutor());
@@ -9335,7 +9335,7 @@ public final class ViewRootImpl implements ViewParent,
getChildVisibleRect(rootView, rect, point);
rootView.dispatchScrollCaptureSearch(rect, point, results::addTarget);
}
Runnable onComplete = () -> dispatchScrollCaptureSearchResult(callbacks, results);
Runnable onComplete = () -> dispatchScrollCaptureSearchResponse(listener, results);
results.setOnCompleteListener(onComplete);
if (!results.isComplete()) {
mHandler.postDelayed(results::finish, getScrollCaptureRequestTimeout());
@@ -9343,8 +9343,8 @@ public final class ViewRootImpl implements ViewParent,
}
/** Called by {@link #handleScrollCaptureRequest} when a result is returned */
private void dispatchScrollCaptureSearchResult(
@NonNull IScrollCaptureCallbacks callbacks,
private void dispatchScrollCaptureSearchResponse(
@NonNull IScrollCaptureResponseListener listener,
@NonNull ScrollCaptureSearchResults results) {
ScrollCaptureTarget selectedTarget = results.getTopResult();
@@ -9361,7 +9361,7 @@ public final class ViewRootImpl implements ViewParent,
if (selectedTarget == null) {
response.setDescription("No scrollable targets found in window");
try {
callbacks.onScrollCaptureResponse(response.build());
listener.onScrollCaptureResponse(response.build());
} catch (RemoteException e) {
Log.e(TAG, "Failed to send scroll capture search result", e);
}
@@ -9387,11 +9387,11 @@ public final class ViewRootImpl implements ViewParent,
// Create a connection and return it to the caller
ScrollCaptureConnection connection = new ScrollCaptureConnection(
mView.getContext().getMainExecutor(), selectedTarget, callbacks);
mView.getContext().getMainExecutor(), selectedTarget);
response.setConnection(connection);
try {
callbacks.onScrollCaptureResponse(response.build());
listener.onScrollCaptureResponse(response.build());
} catch (RemoteException e) {
if (DEBUG_SCROLL_CAPTURE) {
Log.w(TAG, "Failed to send scroll capture search response.", e);
@@ -9691,10 +9691,10 @@ public final class ViewRootImpl implements ViewParent,
}
@Override
public void requestScrollCapture(IScrollCaptureCallbacks callbacks) {
public void requestScrollCapture(IScrollCaptureResponseListener listener) {
final ViewRootImpl viewAncestor = mViewAncestor.get();
if (viewAncestor != null) {
viewAncestor.dispatchScrollCaptureRequest(callbacks);
viewAncestor.dispatchScrollCaptureRequest(listener);
}
}
}

View File

@@ -2619,10 +2619,10 @@ public abstract class Window {
/**
* System request to begin scroll capture.
*
* @param callbacks to receive responses
* @param listener to receive the response
* @hide
*/
public void requestScrollCapture(IScrollCaptureCallbacks callbacks) {
public void requestScrollCapture(IScrollCaptureResponseListener listener) {
}
/**

View File

@@ -78,7 +78,7 @@ import android.view.ContextThemeWrapper;
import android.view.CrossWindowBlurListeners;
import android.view.Gravity;
import android.view.IRotationWatcher.Stub;
import android.view.IScrollCaptureCallbacks;
import android.view.IScrollCaptureResponseListener;
import android.view.IWindowManager;
import android.view.InputDevice;
import android.view.InputEvent;
@@ -3940,12 +3940,12 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
/**
* System request to begin scroll capture.
*
* @param callbacks to receive responses
* @param listener to receive the response
* @hide
*/
@Override
public void requestScrollCapture(IScrollCaptureCallbacks callbacks) {
getViewRootImpl().dispatchScrollCaptureRequest(callbacks);
public void requestScrollCapture(IScrollCaptureResponseListener listener) {
getViewRootImpl().dispatchScrollCaptureRequest(listener);
}
/**

View File

@@ -24,7 +24,7 @@ import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.MergedConfiguration;
import android.view.DragEvent;
import android.view.IScrollCaptureCallbacks;
import android.view.IScrollCaptureResponseListener;
import android.view.IWindow;
import android.view.IWindowSession;
import android.view.InsetsSourceControl;
@@ -159,9 +159,9 @@ public class BaseIWindow extends IWindow.Stub {
}
@Override
public void requestScrollCapture(IScrollCaptureCallbacks callbacks) {
public void requestScrollCapture(IScrollCaptureResponseListener listener) {
try {
callbacks.onScrollCaptureResponse(
listener.onScrollCaptureResponse(
new ScrollCaptureResponse.Builder().setDescription("Not Implemented").build());
} catch (RemoteException ex) {

View File

@@ -74,7 +74,7 @@ public class ScrollCaptureConnectionTest {
mTarget = new ScrollCaptureTarget(mView, mLocalVisibleRect, mPositionInWindow, mCallback);
mTarget.setScrollBounds(mScrollBounds);
mConnection = new ScrollCaptureConnection(Runnable::run, mTarget, mRemote);
mConnection = new ScrollCaptureConnection(Runnable::run, mTarget);
}
/** Test creating a client with valid info */
@@ -83,7 +83,7 @@ public class ScrollCaptureConnectionTest {
ScrollCaptureTarget target = new ScrollCaptureTarget(
mView, mLocalVisibleRect, mPositionInWindow, mCallback);
target.setScrollBounds(new Rect(1, 2, 3, 4));
new ScrollCaptureConnection(Runnable::run, target, mRemote);
new ScrollCaptureConnection(Runnable::run, target);
}
/** Test creating a client fails if arguments are not valid. */
@@ -91,20 +91,20 @@ public class ScrollCaptureConnectionTest {
public void testConstruction_requiresScrollBounds() {
try {
mTarget.setScrollBounds(null);
new ScrollCaptureConnection(Runnable::run, mTarget, mRemote);
new ScrollCaptureConnection(Runnable::run, mTarget);
fail("An exception was expected.");
} catch (RuntimeException ex) {
// Ignore, expected.
}
}
/** @see ScrollCaptureConnection#startCapture(Surface) */
/** @see ScrollCaptureConnection#startCapture(Surface, IScrollCaptureCallbacks) */
@Test
public void testStartCapture() throws Exception {
mConnection.startCapture(mSurface);
mConnection.startCapture(mSurface, mRemote);
mCallback.completeStartRequest();
assertTrue(mConnection.isStarted());
assertTrue(mConnection.isActive());
verify(mRemote, times(1)).onCaptureStarted();
verifyNoMoreInteractions(mRemote);
@@ -112,11 +112,11 @@ public class ScrollCaptureConnectionTest {
@Test
public void testStartCapture_cancellation() throws Exception {
ICancellationSignal signal = mConnection.startCapture(mSurface);
ICancellationSignal signal = mConnection.startCapture(mSurface, mRemote);
signal.cancel();
mCallback.completeStartRequest();
assertFalse(mConnection.isStarted());
assertFalse(mConnection.isActive());
verifyNoMoreInteractions(mRemote);
}
@@ -124,7 +124,7 @@ public class ScrollCaptureConnectionTest {
/** @see ScrollCaptureConnection#requestImage(Rect) */
@Test
public void testRequestImage() throws Exception {
mConnection.startCapture(mSurface);
mConnection.startCapture(mSurface, mRemote);
mCallback.completeStartRequest();
reset(mRemote);
@@ -138,7 +138,7 @@ public class ScrollCaptureConnectionTest {
@Test
public void testRequestImage_cancellation() throws Exception {
mConnection.startCapture(mSurface);
mConnection.startCapture(mSurface, mRemote);
mCallback.completeStartRequest();
reset(mRemote);
@@ -152,7 +152,7 @@ public class ScrollCaptureConnectionTest {
/** @see ScrollCaptureConnection#endCapture() */
@Test
public void testEndCapture() throws Exception {
mConnection.startCapture(mSurface);
mConnection.startCapture(mSurface, mRemote);
mCallback.completeStartRequest();
reset(mRemote);
@@ -167,7 +167,7 @@ public class ScrollCaptureConnectionTest {
/** @see ScrollCaptureConnection#endCapture() */
@Test
public void testEndCapture_cancellation() throws Exception {
mConnection.startCapture(mSurface);
mConnection.startCapture(mSurface, mRemote);
mCallback.completeStartRequest();
reset(mRemote);
@@ -179,9 +179,9 @@ public class ScrollCaptureConnectionTest {
}
@Test
public void testClose() throws Exception {
public void testClose() {
mConnection.close();
assertFalse(mConnection.isConnected());
assertFalse(mConnection.isActive());
verifyNoMoreInteractions(mRemote);
}

View File

@@ -205,7 +205,7 @@ public class ViewRootImplTest {
@Test
public void requestScrollCapture_withoutContentRoot() {
final CountDownLatch latch = new CountDownLatch(1);
mViewRootImpl.handleScrollCaptureRequest(new IScrollCaptureCallbacks.Default() {
mViewRootImpl.handleScrollCaptureRequest(new IScrollCaptureResponseListener.Default() {
@Override
public void onScrollCaptureResponse(ScrollCaptureResponse response) {
latch.countDown();
@@ -237,7 +237,7 @@ public class ViewRootImplTest {
final CountDownLatch latch = new CountDownLatch(1);
mViewRootImpl.setScrollCaptureRequestTimeout(100);
mViewRootImpl.handleScrollCaptureRequest(new IScrollCaptureCallbacks.Default() {
mViewRootImpl.handleScrollCaptureRequest(new IScrollCaptureResponseListener.Default() {
@Override
public void onScrollCaptureResponse(ScrollCaptureResponse response) {
latch.countDown();

View File

@@ -32,7 +32,7 @@ import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
import android.view.DragEvent;
import android.view.IScrollCaptureCallbacks;
import android.view.IScrollCaptureResponseListener;
import android.view.IWindow;
import android.view.IWindowManager;
import android.view.IWindowSession;
@@ -369,9 +369,9 @@ public class SystemWindows {
public void requestAppKeyboardShortcuts(IResultReceiver receiver, int deviceId) {}
@Override
public void requestScrollCapture(IScrollCaptureCallbacks callbacks) {
public void requestScrollCapture(IScrollCaptureResponseListener listener) {
try {
callbacks.onScrollCaptureResponse(
listener.onScrollCaptureResponse(
new ScrollCaptureResponse.Builder()
.setDescription("Not Implemented")
.build());

View File

@@ -26,6 +26,8 @@ import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.IntArray;
import android.util.Log;
@@ -93,6 +95,25 @@ public class CropView extends View {
setAccessibilityDelegate(new AccessibilityHelper());
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.mTopBoundary = getTopBoundary();
ss.mBottomBoundary = getBottomBoundary();
return ss;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
setBoundaryTo(CropBoundary.TOP, ss.mTopBoundary);
setBoundaryTo(CropBoundary.BOTTOM, ss.mBottomBoundary);
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
@@ -380,6 +401,44 @@ public class CropView extends View {
*/
void onCropMotionEvent(MotionEvent event, CropBoundary boundary, float boundaryPosition,
int boundaryPositionPx);
}
static class SavedState extends BaseSavedState {
float mTopBoundary;
float mBottomBoundary;
/**
* Constructor called from {@link CropView#onSaveInstanceState()}
*/
SavedState(Parcelable superState) {
super(superState);
}
/**
* Constructor called from {@link #CREATOR}
*/
private SavedState(Parcel in) {
super(in);
mTopBoundary = in.readFloat();
mBottomBoundary = in.readFloat();
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeFloat(mTopBoundary);
out.writeFloat(mBottomBoundary);
}
public static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}

View File

@@ -213,6 +213,20 @@ class ImageExporter {
CompressFormat format;
boolean published;
boolean deleted;
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Result{");
sb.append("uri=").append(uri);
sb.append(", requestId=").append(requestId);
sb.append(", fileName='").append(fileName).append('\'');
sb.append(", timestamp=").append(timestamp);
sb.append(", format=").append(format);
sb.append(", published=").append(published);
sb.append(", deleted=").append(deleted);
sb.append('}');
return sb.toString();
}
}
private static class Task {

View File

@@ -21,7 +21,6 @@ import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import androidx.concurrent.futures.CallbackToFutureAdapter;
@@ -43,6 +42,16 @@ public class ImageLoader {
@Nullable Uri uri;
@Nullable File fileName;
@Nullable Bitmap bitmap;
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Result{");
sb.append("uri=").append(uri);
sb.append(", fileName=").append(fileName);
sb.append(", bitmap=").append(bitmap);
sb.append('}');
return sb.toString();
}
}
@Inject
@@ -54,7 +63,7 @@ public class ImageLoader {
* Loads an image via URI from ContentResolver.
*
* @param uri the identifier of the image to load
* @return a listenable future result
* @return a listenable future result containing a bitmap
*/
ListenableFuture<Result> load(Uri uri) {
return CallbackToFutureAdapter.getFuture(completer -> {
@@ -76,7 +85,7 @@ public class ImageLoader {
* permissions to read this file/path.
*
* @param file the system file path of the image to load
* @return a listenable future result
* @return a listenable future result containing a bitmap
*/
ListenableFuture<Result> load(File file) {
return CallbackToFutureAdapter.getFuture(completer -> {

View File

@@ -31,6 +31,7 @@ import com.android.internal.util.CallbackRegistry;
import com.android.internal.util.CallbackRegistry.NotifierCallback;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
@@ -69,9 +70,6 @@ class ImageTileSet {
private final Rect mBounds = new Rect();
private final Handler mHandler;
private OnContentChangedListener mOnContentChangedListener;
private OnBoundsChangedListener mOnBoundsChangedListener;
void addOnBoundsChangedListener(OnBoundsChangedListener listener) {
if (mOnBoundsListeners == null) {
mOnBoundsListeners = new CallbackRegistry<>(
@@ -204,18 +202,17 @@ class ImageTileSet {
return mBounds.height();
}
@AnyThread
void clear() {
if (!mHandler.getLooper().isCurrentThread()) {
mHandler.post(this::clear);
return;
}
if (mTiles.isEmpty()) {
return;
}
mBounds.setEmpty();
mTiles.forEach(ImageTile::close);
mTiles.clear();
Iterator<ImageTile> i = mTiles.iterator();
while (i.hasNext()) {
ImageTile next = i.next();
next.close();
i.remove();
}
notifyBoundsChanged(mBounds);
notifyContentChanged();
}

View File

@@ -21,7 +21,6 @@ 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;
@@ -30,10 +29,13 @@ 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.RemoteException;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
import android.view.IScrollCaptureConnection;
import android.view.IWindowManager;
import android.view.ScrollCaptureResponse;
import android.view.View;
import android.widget.ImageView;
@@ -41,14 +43,14 @@ import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.screenshot.ScrollCaptureController.LongScreenshot;
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.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
@@ -61,18 +63,15 @@ 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";
public static final String EXTRA_CAPTURE_RESPONSE = "capture-response";
private static final String KEY_SAVED_IMAGE_PATH = "saved-image-path";
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;
@@ -84,6 +83,14 @@ public class LongScreenshotActivity extends Activity {
private View mShare;
private CropView mCropView;
private MagnifierView mMagnifierView;
private ScrollCaptureResponse mScrollCaptureResponse;
private File mSavedImagePath;
private ListenableFuture<File> mCacheSaveFuture;
private ListenableFuture<ImageLoader.Result> mCacheLoadFuture;
private ListenableFuture<LongScreenshot> mLongScreenshotFuture;
private LongScreenshot mLongScreenshot;
private enum PendingAction {
SHARE,
@@ -92,35 +99,31 @@ public class LongScreenshotActivity extends Activity {
}
@Inject
public LongScreenshotActivity(UiEventLogger uiEventLogger,
ImageExporter imageExporter,
@Main Executor mainExecutor,
@Background Executor bgExecutor,
public LongScreenshotActivity(UiEventLogger uiEventLogger, ImageExporter imageExporter,
@Main Executor mainExecutor, @Background Executor bgExecutor, IWindowManager wms,
Context context) {
mUiEventLogger = uiEventLogger;
mUiExecutor = mainExecutor;
mBackgroundExecutor = bgExecutor;
mImageExporter = imageExporter;
mScrollCaptureController = new ScrollCaptureController(context, mainExecutor, bgExecutor,
imageExporter);
mConnection = ScreenshotController.takeScrollCaptureConnection();
mScrollCaptureController = new ScrollCaptureController(context, bgExecutor, wms);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate(savedInstanceState = " + savedInstanceState + ")");
super.onCreate(savedInstanceState);
setContentView(R.layout.long_screenshot);
mPreview = findViewById(R.id.preview);
mSave = findViewById(R.id.save);
mCancel = findViewById(R.id.cancel);
mEdit = findViewById(R.id.edit);
mShare = findViewById(R.id.share);
mCropView = findViewById(R.id.crop_view);
mMagnifierView = findViewById(R.id.magnifier);
mPreview = requireViewById(R.id.preview);
mSave = requireViewById(R.id.save);
mCancel = requireViewById(R.id.cancel);
mEdit = requireViewById(R.id.edit);
mShare = requireViewById(R.id.share);
mCropView = requireViewById(R.id.crop_view);
mMagnifierView = requireViewById(R.id.magnifier);
mCropView.setCropInteractionListener(mMagnifierView);
mSave.setOnClickListener(this::onClicked);
@@ -128,69 +131,186 @@ public class LongScreenshotActivity extends Activity {
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);
});
}
});
}
}
mPreview.addOnLayoutChangeListener(
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
updateCropLocation());
Intent intent = getIntent();
mScrollCaptureResponse = intent.getParcelableExtra(EXTRA_CAPTURE_RESPONSE);
if (savedInstanceState != null) {
String savedImagePath = savedInstanceState.getString(KEY_SAVED_IMAGE_PATH);
if (savedImagePath == null) {
Log.e(TAG, "Missing saved state entry with key '" + KEY_SAVED_IMAGE_PATH + "'!");
finishAndRemoveTask();
return;
}
mSavedImagePath = new File(savedImagePath);
ImageLoader imageLoader = new ImageLoader(getContentResolver());
mCacheLoadFuture = imageLoader.load(mSavedImagePath);
}
}
@Override
public void onStart() {
Log.d(TAG, "onStart");
super.onStart();
if (mPreview.getDrawable() == null && !mRestoringInstance) {
if (mConnection == null) {
Log.e(TAG, "Failed to get scroll capture connection, bailing out");
if (mCacheLoadFuture != null) {
Log.d(TAG, "mRestoringInstance = true");
final ListenableFuture<ImageLoader.Result> future = mCacheLoadFuture;
mCacheLoadFuture.addListener(() -> {
Log.d(TAG, "cached bitmap load complete");
try {
onCachedImageLoaded(future.get());
} catch (CancellationException | ExecutionException | InterruptedException e) {
Log.e(TAG, "Failed to load cached image", e);
if (mSavedImagePath != null) {
//noinspection ResultOfMethodCallIgnored
mSavedImagePath.delete();
mSavedImagePath = null;
}
finishAndRemoveTask();
}
}, mUiExecutor);
mCacheLoadFuture = null;
return;
}
if (mLongScreenshotFuture == null) {
Log.d(TAG, "mLongScreenshotFuture == null");
// First run through, ensure we have a connection to use (see #onCreate)
if (mScrollCaptureResponse == null || !mScrollCaptureResponse.isConnected()) {
Log.e(TAG, "Did not receive a live scroll capture connection, bailing out!");
finishAndRemoveTask();
return;
}
doCapture();
mLongScreenshotFuture = mScrollCaptureController.run(mScrollCaptureResponse);
mLongScreenshotFuture.addListener(() -> {
LongScreenshot longScreenshot;
try {
longScreenshot = mLongScreenshotFuture.get();
} catch (CancellationException | InterruptedException | ExecutionException e) {
Log.e(TAG, "Error capturing long screenshot!", e);
finishAndRemoveTask();
return;
}
if (longScreenshot.getHeight() == 0) {
Log.e(TAG, "Got a zero height result");
finishAndRemoveTask();
return;
}
onCaptureCompleted(longScreenshot);
}, mUiExecutor);
} else {
Log.d(TAG, "mLongScreenshotFuture != null");
}
}
private void onCaptureCompleted(LongScreenshot longScreenshot) {
Log.d(TAG, "onCaptureCompleted(longScreenshot=" + longScreenshot + ")");
mLongScreenshot = longScreenshot;
mPreview.setImageDrawable(mLongScreenshot.getDrawable());
updateCropLocation();
mMagnifierView.setDrawable(mLongScreenshot.getDrawable(),
mLongScreenshot.getWidth(), mLongScreenshot.getHeight());
// Original boundaries go from the image tile set's y=0 to y=pageSize, so
// we animate to that as a starting crop position.
float topFraction = Math.max(0,
-mLongScreenshot.getTop() / (float) mLongScreenshot.getHeight());
float bottomFraction = Math.min(1f,
1 - (mLongScreenshot.getBottom() - mLongScreenshot.getPageHeight())
/ (float) mLongScreenshot.getHeight());
mCropView.animateBoundaryTo(CropView.CropBoundary.TOP, topFraction);
mCropView.animateBoundaryTo(CropView.CropBoundary.BOTTOM, bottomFraction);
setButtonsEnabled(true);
// Immediately export to temp image file for saved state
mCacheSaveFuture = mImageExporter.exportAsTempFile(mBackgroundExecutor,
mLongScreenshot.toBitmap());
mCacheSaveFuture.addListener(() -> {
try {
// Get the temp file path to persist, used in onSavedInstanceState
mSavedImagePath = mCacheSaveFuture.get();
} catch (CancellationException | InterruptedException | ExecutionException e) {
Log.e(TAG, "Error saving temp image file", e);
finishAndRemoveTask();
}
}, mUiExecutor);
}
private void onCachedImageLoaded(ImageLoader.Result imageResult) {
Log.d(TAG, "onCachedImageLoaded(imageResult=" + imageResult + ")");
BitmapDrawable drawable = new BitmapDrawable(getResources(), imageResult.bitmap);
mPreview.setImageDrawable(drawable);
mMagnifierView.setDrawable(drawable, imageResult.bitmap.getWidth(),
imageResult.bitmap.getHeight());
mSavedImagePath = imageResult.fileName;
setButtonsEnabled(true);
}
private static Bitmap renderBitmap(Drawable drawable, Rect bounds) {
final RenderNode output = new RenderNode("Bitmap Export");
output.setPosition(0, 0, bounds.width(), bounds.height());
RecordingCanvas canvas = output.beginRecording();
canvas.translate(-bounds.left, -bounds.top);
canvas.clipRect(bounds);
drawable.draw(canvas);
output.endRecording();
return HardwareRenderer.createHardwareBitmap(output, bounds.width(), bounds.height());
}
@Override
protected void onSaveInstanceState(Bundle outState) {
Log.d(TAG, "onSaveInstanceState");
super.onSaveInstanceState(outState);
outState.putString(IMAGE_PATH_KEY, mSavedImagePath);
outState.putFloat(TOP_BOUNDARY_KEY, mCropView.getTopBoundary());
outState.putFloat(BOTTOM_BOUNDARY_KEY, mCropView.getBottomBoundary());
if (mSavedImagePath != null) {
outState.putString(KEY_SAVED_IMAGE_PATH, mSavedImagePath.getPath());
}
}
@Override
protected void onPause() {
Log.d(TAG, "onPause finishing=" + isFinishing());
super.onPause();
if (isFinishing()) {
if (mScrollCaptureResponse != null) {
mScrollCaptureResponse.close();
}
cleanupCache();
if (mLongScreenshotFuture != null) {
mLongScreenshotFuture.cancel(true);
}
if (mLongScreenshot != null) {
mLongScreenshot.release();
}
}
}
void cleanupCache() {
if (mCacheSaveFuture != null) {
mCacheSaveFuture.cancel(true);
}
if (mSavedImagePath != null) {
Log.d(TAG, "Deleting " + mSavedImagePath);
//noinspection ResultOfMethodCallIgnored
mSavedImagePath.delete();
mSavedImagePath = null;
}
}
@Override
protected void onStop() {
Log.d(TAG, "onStop");
super.onStop();
}
@Override
protected void onDestroy() {
Log.d(TAG, "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) {
@@ -244,67 +364,51 @@ public class LongScreenshotActivity extends Activity {
}
private void startExport(PendingAction action) {
Log.d(TAG, "startExport(action = " + action + ")");
Drawable drawable = mPreview.getDrawable();
if (drawable == null) {
Log.e(TAG, "No drawable, skipping export!");
return;
}
Rect croppedPortion = new Rect(
0,
(int) (drawable.getIntrinsicHeight() * mCropView.getTopBoundary()),
drawable.getIntrinsicWidth(),
(int) (drawable.getIntrinsicHeight() * mCropView.getBottomBoundary()));
Rect bounds = new Rect(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
int height = bounds.height();
bounds.top = (int) (height * mCropView.getTopBoundary());
bounds.bottom = (int) (height * mCropView.getBottomBoundary());
if (bounds.isEmpty()) {
Log.w(TAG, "Crop bounds empty, skipping export.");
return;
}
Bitmap output = renderBitmap(mPreview.getDrawable(), bounds);
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);
mBackgroundExecutor, UUID.randomUUID(), output, ZonedDateTime.now());
exportFuture.addListener(() -> onExportCompleted(action, exportFuture), 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());
private void onExportCompleted(PendingAction action,
ListenableFuture<ImageExporter.Result> exportFuture) {
setButtonsEnabled(true);
ImageExporter.Result result;
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);
result = exportFuture.get();
} catch (CancellationException | InterruptedException | ExecutionException e) {
Log.e(TAG, "failed to export", e);
return;
}
switch (action) {
case EDIT:
doEdit(result.uri);
break;
case SHARE:
doShare(result.uri);
break;
case SAVE:
// Nothing more to do
finishAndRemoveTask();
break;
}
}
@@ -313,8 +417,8 @@ public class LongScreenshotActivity extends Activity {
if (drawable == null) {
return;
}
float imageRatio = drawable.getBounds().width() / (float) drawable.getBounds().height();
Rect bounds = drawable.getBounds();
float imageRatio = bounds.width() / (float) bounds.height();
float viewRatio = mPreview.getWidth() / (float) mPreview.getHeight();
if (imageRatio > viewRatio) {
@@ -328,35 +432,4 @@ public class LongScreenshotActivity extends Activity {
mCropView.setExtraPadding(0, 0);
}
}
private void doCapture() {
mScrollCaptureController.start(mConnection,
new ScrollCaptureController.ScrollCaptureCallback() {
@Override
public void onError() {
Log.e(TAG, "Error capturing long screenshot!");
finishAndRemoveTask();
}
@Override
public void onComplete(ImageTileSet imageTileSet, int pageSize) {
Log.i(TAG, "Got tiles " + imageTileSet.getWidth() + " x "
+ imageTileSet.getHeight());
mPreview.setImageDrawable(imageTileSet.getDrawable());
updateCropLocation();
mMagnifierView.setDrawable(imageTileSet.getDrawable(),
imageTileSet.getWidth(), imageTileSet.getHeight());
// Original boundaries go from the image tile set's y=0 to y=pageSize, so
// we animate to that as a starting crop position.
float topFraction = Math.max(0,
-imageTileSet.getTop() / (float) imageTileSet.getHeight());
float bottomFraction = Math.min(1f,
1 - (imageTileSet.getBottom() - pageSize)
/ (float) imageTileSet.getHeight());
mCropView.animateBoundaryTo(CropView.CropBoundary.TOP, topFraction);
mCropView.animateBoundaryTo(CropView.CropBoundary.BOTTOM, bottomFraction);
mBackgroundExecutor.execute(() -> saveCacheBitmap(imageTileSet));
}
});
}
}

View File

@@ -65,6 +65,7 @@ import android.view.DisplayAddress;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.ScrollCaptureResponse;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewTreeObserver;
@@ -86,10 +87,14 @@ import com.android.systemui.screenshot.ScreenshotController.SavedImageData.Actio
import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback;
import com.android.systemui.util.DeviceConfigProxy;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -101,7 +106,8 @@ import javax.inject.Inject;
public class ScreenshotController {
private static final String TAG = logTag(ScreenshotController.class);
private static ScrollCaptureClient.Connection sScrollConnection;
private ScrollCaptureResponse mLastScrollCaptureResponse;
private ListenableFuture<ScrollCaptureResponse> mLastScrollCaptureRequest;
/**
* POD used in the AsyncTask which saves an image in the background.
@@ -222,12 +228,6 @@ public class ScreenshotController {
| ActivityInfo.CONFIG_SCREEN_LAYOUT
| ActivityInfo.CONFIG_ASSETS_PATHS);
public static @Nullable ScrollCaptureClient.Connection takeScrollCaptureConnection() {
ScrollCaptureClient.Connection connection = sScrollConnection;
sScrollConnection = null;
return connection;
}
@Inject
ScreenshotController(
Context context,
@@ -352,6 +352,11 @@ public class ScreenshotController {
} else {
mScreenshotView.animateDismissal();
}
if (mLastScrollCaptureResponse != null) {
mLastScrollCaptureResponse.close();
mLastScrollCaptureResponse = null;
}
}
boolean isPendingSharedTransition() {
@@ -526,16 +531,15 @@ public class ScreenshotController {
// the reference used to locate the target window (below).
withWindowAttached(() -> {
mScrollCaptureClient.setHostWindowToken(decorView.getWindowToken());
mScrollCaptureClient.request(DEFAULT_DISPLAY,
/* onConnection */
(connection) -> mScreenshotHandler.post(() ->
mScreenshotView.showScrollChip(() ->
/* onClick */
runScrollCapture(connection))));
if (mLastScrollCaptureRequest != null) {
mLastScrollCaptureRequest.cancel(true);
}
mLastScrollCaptureRequest = mScrollCaptureClient.request(DEFAULT_DISPLAY);
mLastScrollCaptureRequest.addListener(() ->
onScrollCaptureResponseReady(mLastScrollCaptureRequest), mMainExecutor);
});
}
attachWindow();
mScreenshotView.getViewTreeObserver().addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener() {
@@ -560,6 +564,27 @@ public class ScreenshotController {
cancelTimeout(); // restarted after animation
}
private void onScrollCaptureResponseReady(Future<ScrollCaptureResponse> responseFuture) {
try {
if (mLastScrollCaptureResponse != null) {
mLastScrollCaptureResponse.close();
}
mLastScrollCaptureResponse = responseFuture.get();
final Intent intent = new Intent(mContext, LongScreenshotActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtra(LongScreenshotActivity.EXTRA_CAPTURE_RESPONSE,
mLastScrollCaptureResponse);
mScreenshotView.showScrollChip(/* onClick */ () -> {
// Clear the reference to prevent close() in dismissScreenshot
mLastScrollCaptureResponse = null;
mContext.startActivity(intent);
dismissScreenshot(false);
});
} catch (InterruptedException | ExecutionException e) {
Log.e(TAG, "requestScrollCapture failed", e);
}
}
private void withWindowAttached(Runnable action) {
View decorView = mWindow.getDecorView();
if (decorView.isAttachedToWindow()) {
@@ -606,15 +631,6 @@ public class ScreenshotController {
}
}
private void runScrollCapture(ScrollCaptureClient.Connection connection) {
sScrollConnection = connection; // For LongScreenshotActivity to pick up.
Intent intent = new Intent(mContext, LongScreenshotActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
mContext.startActivity(intent);
dismissScreenshot(false);
}
/**
* Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on
* failure).

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 The Android Open Source Project
* 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.
@@ -30,18 +30,23 @@ import android.graphics.Rect;
import android.hardware.HardwareBuffer;
import android.media.Image;
import android.media.ImageReader;
import android.os.DeadObjectException;
import android.os.IBinder;
import android.os.ICancellationSignal;
import android.os.RemoteException;
import android.util.Log;
import android.view.IScrollCaptureCallbacks;
import android.view.IScrollCaptureConnection;
import android.view.IScrollCaptureResponseListener;
import android.view.IWindowManager;
import android.view.ScrollCaptureResponse;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
import com.android.internal.annotations.VisibleForTesting;
import java.util.function.Consumer;
import com.google.common.util.concurrent.ListenableFuture;
import javax.inject.Inject;
@@ -57,61 +62,6 @@ public class ScrollCaptureClient {
private static final String TAG = LogConfig.logTag(ScrollCaptureClient.class);
/**
* A connection to a remote window. Starts a capture session.
*/
public interface Connection {
/**
* Start a session.
* @param sessionConsumer listener to receive the session once active
* @param maxPages the capture buffer size expressed as a multiple of the content height
*/
// TODO ListenableFuture
void start(Consumer<Session> sessionConsumer, float maxPages);
/**
* Close the connection. Must end capture if started to avoid potential unwanted visual
* artifacts.
*
* @see Session#end(Runnable)
*/
void close();
}
static class CaptureResult {
public final Image image;
/**
* The area requested, in content rect space, relative to scroll-bounds.
*/
public final Rect requested;
/**
* The actual area captured, in content rect space, relative to scroll-bounds. This may be
* cropped or empty depending on available content.
*/
public final Rect captured;
// Error?
private CaptureResult(Image image, Rect request, Rect captured) {
this.image = image;
this.requested = request;
this.captured = captured;
}
@Override
public String toString() {
return "CaptureResult{"
+ "requested=" + requested
+ " (" + requested.width() + "x" + requested.height() + ")"
+ ", captured=" + captured
+ " (" + captured.width() + "x" + captured.height() + ")"
+ ", image=" + image
+ '}';
}
}
/**
* Represents the connection to a target window and provides a mechanism for requesting tiles.
*/
@@ -121,10 +71,8 @@ public class ScrollCaptureClient {
* and from left 0, to {@link #getPageWidth()}
*
* @param top the top (y) position of the tile to capture, in content rect space
* @param consumer listener to be informed of the result
*/
// TODO ListenableFuture
void requestTile(int top, Consumer<CaptureResult> consumer);
ListenableFuture<CaptureResult> requestTile(int top);
/**
* Returns the maximum number of tiles which may be requested and retained without
@@ -139,6 +87,7 @@ public class ScrollCaptureClient {
*/
int getTileHeight();
/**
* @return the height of scrollable content being captured
*/
@@ -155,11 +104,42 @@ public class ScrollCaptureClient {
Rect getWindowBounds();
/**
* End the capture session, return the target app to original state. The listener
* will be called when the target app is ready to before visible and interactive.
* End the capture session, return the target app to original state. The returned Future
* will complete once the target app is ready to become visible and interactive.
*/
// TODO ListenableFuture
void end(Runnable listener);
ListenableFuture<Void> end();
void release();
}
static class CaptureResult {
public final Image image;
/**
* The area requested, in content rect space, relative to scroll-bounds.
*/
public final Rect requested;
/**
* The actual area captured, in content rect space, relative to scroll-bounds. This may be
* cropped or empty depending on available content.
*/
public final Rect captured;
CaptureResult(Image image, Rect request, Rect captured) {
this.image = image;
this.requested = request;
this.captured = captured;
}
@Override
public String toString() {
return "CaptureResult{"
+ "requested=" + requested
+ " (" + requested.width() + "x" + requested.height() + ")"
+ ", captured=" + captured
+ " (" + captured.width() + "x" + captured.height() + ")"
+ ", image=" + image
+ '}';
}
}
private final IWindowManager mWindowManagerService;
@@ -185,10 +165,9 @@ public class ScrollCaptureClient {
* Check for scroll capture support.
*
* @param displayId id for the display containing the target window
* @param consumer receives a connection when available
*/
public void request(int displayId, Consumer<Connection> consumer) {
request(displayId, MATCH_ANY_TASK, consumer);
public ListenableFuture<ScrollCaptureResponse> request(int displayId) {
return request(displayId, MATCH_ANY_TASK);
}
/**
@@ -196,195 +175,210 @@ public class ScrollCaptureClient {
*
* @param displayId id for the display containing the target window
* @param taskId id for the task containing the target window or {@link #MATCH_ANY_TASK}.
* @param consumer receives a connection when available
* @return a listenable future providing the response
*/
public void request(int displayId, int taskId, Consumer<Connection> consumer) {
try {
if (DEBUG_SCROLL) {
Log.d(TAG, "requestScrollCapture(displayId=" + displayId + ", " + mHostWindowToken
+ ", taskId=" + taskId + ", consumer=" + consumer + ")");
public ListenableFuture<ScrollCaptureResponse> request(int displayId, int taskId) {
return CallbackToFutureAdapter.getFuture((completer) -> {
try {
mWindowManagerService.requestScrollCapture(displayId, mHostWindowToken, taskId,
new IScrollCaptureResponseListener.Stub() {
@Override
public void onScrollCaptureResponse(ScrollCaptureResponse response) {
completer.set(response);
}
});
} catch (RemoteException e) {
completer.setException(e);
}
mWindowManagerService.requestScrollCapture(displayId, mHostWindowToken, taskId,
new ClientCallbacks(consumer));
} catch (RemoteException e) {
Log.e(TAG, "Ignored remote exception", e);
}
return "ScrollCaptureClient#request"
+ "(displayId=" + displayId + ", taskId=" + taskId + ")";
});
}
private static class ClientCallbacks extends IScrollCaptureCallbacks.Stub implements
Connection, Session, IBinder.DeathRecipient {
/**
* Start a scroll capture session.
*
* @param response a response provided from a request containing a connection
* @param maxPages the capture buffer size expressed as a multiple of the content height
* @return a listenable future providing the session
*/
public ListenableFuture<Session> start(ScrollCaptureResponse response, float maxPages) {
IScrollCaptureConnection connection = response.getConnection();
return CallbackToFutureAdapter.getFuture((completer) -> {
if (connection == null || !connection.asBinder().isBinderAlive()) {
completer.setException(new DeadObjectException("No active connection!"));
return "";
}
SessionWrapper session = new SessionWrapper(connection, response.getWindowBounds(),
response.getBoundsInWindow(), maxPages);
session.start(completer);
return "IScrollCaptureCallbacks#onCaptureStarted";
});
}
private static class SessionWrapper extends IScrollCaptureCallbacks.Stub implements Session,
IBinder.DeathRecipient {
private IScrollCaptureConnection mConnection;
private Consumer<Connection> mConnectionConsumer;
private Consumer<Session> mSessionConsumer;
private Consumer<CaptureResult> mResultConsumer;
private Runnable mShutdownListener;
private ImageReader mReader;
private Rect mScrollBounds;
private int mTileHeight;
private int mTileWidth;
private final int mTileHeight;
private final int mTileWidth;
private Rect mRequestRect;
private boolean mStarted;
private ICancellationSignal mCancellationSignal;
private Rect mWindowBounds;
private Rect mBoundsInWindow;
private int mMaxTiles;
private final Rect mWindowBounds;
private final Rect mBoundsInWindow;
private final int mMaxTiles;
private ClientCallbacks(Consumer<Connection> connectionConsumer) {
mConnectionConsumer = connectionConsumer;
}
private Completer<Session> mStartCompleter;
private Completer<CaptureResult> mTileRequestCompleter;
private Completer<Void> mEndCompleter;
@BinderThread
@Override
public void onScrollCaptureResponse(ScrollCaptureResponse response) throws RemoteException {
if (DEBUG_SCROLL) {
Log.d(TAG, "onScrollCaptureResponse(response=" + response + ")");
}
if (response.isConnected()) {
mConnection = response.getConnection();
mConnection.asBinder().linkToDeath(this, 0);
mWindowBounds = response.getWindowBounds();
mBoundsInWindow = response.getBoundsInWindow();
private SessionWrapper(IScrollCaptureConnection connection, Rect windowBounds,
Rect boundsInWindow, float maxPages) throws RemoteException {
mConnection = requireNonNull(connection);
mConnection.asBinder().linkToDeath(SessionWrapper.this, 0);
mWindowBounds = requireNonNull(windowBounds);
mBoundsInWindow = requireNonNull(boundsInWindow);
int pxPerPage = mBoundsInWindow.width() * mBoundsInWindow.height();
int pxPerTile = min(TILE_SIZE_PX_MAX, (pxPerPage / TILES_PER_PAGE));
mTileWidth = mBoundsInWindow.width();
mTileHeight = pxPerTile / mBoundsInWindow.width();
if (DEBUG_SCROLL) {
Log.d(TAG, "boundsInWindow: " + mBoundsInWindow);
Log.d(TAG, "tile size: " + mTileWidth + "x" + mTileHeight);
Log.d(TAG, "maxHeight: " + (mMaxTiles * mTileHeight) + "px");
}
mConnectionConsumer.accept(this);
}
mConnectionConsumer = null;
}
int pxPerPage = mBoundsInWindow.width() * mBoundsInWindow.height();
int pxPerTile = min(TILE_SIZE_PX_MAX, (pxPerPage / TILES_PER_PAGE));
@Override
public void start(Consumer<Session> sessionConsumer, float maxPages) {
if (DEBUG_SCROLL) {
Log.d(TAG, "start(sessionConsumer=" + sessionConsumer + ","
+ " maxPages=" + maxPages + ")");
}
mTileWidth = mBoundsInWindow.width();
mTileHeight = pxPerTile / mBoundsInWindow.width();
mMaxTiles = (int) Math.ceil(maxPages * TILES_PER_PAGE);
if (DEBUG_SCROLL) {
Log.d(TAG, "boundsInWindow: " + mBoundsInWindow);
Log.d(TAG, "tile size: " + mTileWidth + "x" + mTileHeight);
}
}
@Override
public void binderDied() {
Log.d(TAG, "binderDied! The target process just crashed :-(");
// Clean up
mConnection = null;
// Pass along the bad news.
if (mStartCompleter != null) {
mStartCompleter.setException(new DeadObjectException("The remote process died"));
}
if (mTileRequestCompleter != null) {
mTileRequestCompleter.setException(
new DeadObjectException("The remote process died"));
}
if (mEndCompleter != null) {
mEndCompleter.setException(new DeadObjectException("The remote process died"));
}
}
private void start(Completer<Session> completer) {
mReader = ImageReader.newInstance(mTileWidth, mTileHeight, PixelFormat.RGBA_8888,
mMaxTiles, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
mSessionConsumer = sessionConsumer;
mStartCompleter = completer;
try {
mCancellationSignal = mConnection.startCapture(mReader.getSurface());
mCancellationSignal = mConnection.startCapture(mReader.getSurface(), this);
completer.addCancellationListener(() -> {
try {
mCancellationSignal.cancel();
} catch (RemoteException e) {
// Ignore
}
}, Runnable::run);
mStarted = true;
} catch (RemoteException e) {
Log.w(TAG, "Failed to start", e);
mReader.close();
completer.setException(e);
}
}
@BinderThread
@Override
public void onCaptureStarted() {
if (DEBUG_SCROLL) {
Log.d(TAG, "onCaptureStarted()");
}
mSessionConsumer.accept(this);
mSessionConsumer = null;
Log.d(TAG, "onCaptureStarted");
mStartCompleter.set(this);
}
@Override
public void requestTile(int top, Consumer<CaptureResult> consumer) {
if (DEBUG_SCROLL) {
Log.d(TAG, "requestTile(top=" + top + ", consumer=" + consumer + ")");
}
cancelPendingRequest();
public ListenableFuture<CaptureResult> requestTile(int top) {
mRequestRect = new Rect(0, top, mTileWidth, top + mTileHeight);
mResultConsumer = consumer;
try {
mCancellationSignal = mConnection.requestImage(mRequestRect);
} catch (RemoteException e) {
Log.e(TAG, "Caught remote exception from requestImage", e);
}
return CallbackToFutureAdapter.getFuture((completer -> {
if (mConnection == null || !mConnection.asBinder().isBinderAlive()) {
completer.setException(new DeadObjectException("Connection is closed!"));
return "";
}
try {
mTileRequestCompleter = completer;
mCancellationSignal = mConnection.requestImage(mRequestRect);
completer.addCancellationListener(() -> {
try {
mCancellationSignal.cancel();
} catch (RemoteException e) {
// Ignore
}
}, Runnable::run);
} catch (RemoteException e) {
completer.setException(e);
}
return "IScrollCaptureCallbacks#onImageRequestCompleted";
}));
}
@BinderThread
@Override
public void onImageRequestCompleted(int flags, Rect contentArea) {
Image image = mReader.acquireLatestImage();
if (DEBUG_SCROLL) {
Log.d(TAG, "onCaptureBufferSent(flags=" + flags
+ ", contentArea=" + contentArea + ") image=" + image);
}
// Save and clear first, since the consumer will likely request the next
// tile, otherwise the new consumer will be wiped out.
Consumer<CaptureResult> consumer = mResultConsumer;
mResultConsumer = null;
consumer.accept(new CaptureResult(image, mRequestRect, contentArea));
mTileRequestCompleter.set(new CaptureResult(image, mRequestRect, contentArea));
}
@Override
public void end(Runnable listener) {
if (DEBUG_SCROLL) {
Log.d(TAG, "end(listener=" + listener + ")");
}
if (mStarted) {
mShutdownListener = listener;
mReader.close();
public ListenableFuture<Void> end() {
Log.d(TAG, "end()");
return CallbackToFutureAdapter.getFuture(completer -> {
if (!mStarted) {
try {
mConnection.asBinder().unlinkToDeath(SessionWrapper.this, 0);
mConnection.close();
} catch (RemoteException e) {
/* ignore */
}
mConnection = null;
completer.set(null);
return "";
}
mEndCompleter = completer;
try {
// listener called from onConnectionClosed callback
mConnection.endCapture();
} catch (RemoteException e) {
Log.d(TAG, "Ignored exception from endCapture()", e);
disconnect();
listener.run();
completer.setException(e);
}
} else {
disconnect();
listener.run();
}
return "IScrollCaptureCallbacks#onCaptureEnded";
});
}
public void release() {
mReader.close();
}
@BinderThread
@Override
public void onCaptureEnded() {
close();
if (mShutdownListener != null) {
mShutdownListener.run();
mShutdownListener = null;
}
}
@Override
public void close() {
if (mConnection != null) {
try {
mConnection.close();
} catch (RemoteException e) {
/* ignore */
}
disconnect();
try {
mConnection.close();
} catch (RemoteException e) {
/* ignore */
}
mConnection = null;
mEndCompleter.set(null);
}
// Misc
private void disconnect() {
if (mConnection != null) {
mConnection.asBinder().unlinkToDeath(this, 0);
}
mConnection = null;
}
/**
* The process hosting the window went away abruptly!
*/
@Override
public void binderDied() {
if (DEBUG_SCROLL) {
Log.d(TAG, "binderDied()");
}
disconnect();
}
@Override
public int getPageHeight() {
return mBoundsInWindow.height();
@@ -408,16 +402,5 @@ public class ScrollCaptureClient {
public int getMaxTiles() {
return mMaxTiles;
}
private void cancelPendingRequest() {
if (mCancellationSignal != null) {
try {
mCancellationSignal.cancel();
} catch (RemoteException e) {
/* ignore */
}
mCancellationSignal = null;
}
}
}
}

View File

@@ -16,18 +16,27 @@
package com.android.systemui.screenshot;
import android.annotation.UiThread;
import android.content.Context;
import android.net.Uri;
import android.graphics.Bitmap;
import android.graphics.HardwareRenderer;
import android.graphics.RecordingCanvas;
import android.graphics.Rect;
import android.graphics.RenderNode;
import android.graphics.drawable.Drawable;
import android.provider.Settings;
import android.util.Log;
import android.view.IWindowManager;
import android.view.ScrollCaptureResponse;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
import com.android.systemui.screenshot.ScrollCaptureClient.CaptureResult;
import com.android.systemui.screenshot.ScrollCaptureClient.Connection;
import com.android.systemui.screenshot.ScrollCaptureClient.Session;
import java.time.ZonedDateTime;
import java.util.UUID;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
/**
@@ -47,44 +56,136 @@ public class ScrollCaptureController {
// or if the desired bitmap size is reached.
private boolean mFinishOnBoundary;
private Session mSession;
public static final int MAX_HEIGHT = 12000;
private final Context mContext;
private final Executor mUiExecutor;
private final Executor mBgExecutor;
private final ImageExporter mImageExporter;
private final ImageTileSet mImageTileSet;
private final ScrollCaptureClient mClient;
private ZonedDateTime mCaptureTime;
private UUID mRequestId;
private ScrollCaptureCallback mCaptureCallback;
private Completer<LongScreenshot> mCaptureCompleter;
public ScrollCaptureController(Context context, Executor uiExecutor, Executor bgExecutor,
ImageExporter exporter) {
private ListenableFuture<Session> mSessionFuture;
private Session mSession;
private ListenableFuture<CaptureResult> mTileFuture;
private ListenableFuture<Void> mEndFuture;
static class LongScreenshot {
private final ImageTileSet mImageTileSet;
private final Session mSession;
LongScreenshot(Session session, ImageTileSet imageTileSet) {
mSession = session;
mImageTileSet = imageTileSet;
}
/** Returns a bitmap containing the combinded result. */
public Bitmap toBitmap() {
return mImageTileSet.toBitmap();
}
public Bitmap toBitmap(Rect bounds) {
return mImageTileSet.toBitmap(bounds);
}
/** Releases image resources from the screenshot. */
public void release() {
Log.d(TAG, "LongScreenshot :: release()");
mImageTileSet.clear();
mSession.release();
}
public int getLeft() {
return mImageTileSet.getLeft();
}
public int getTop() {
return mImageTileSet.getTop();
}
public int getBottom() {
return mImageTileSet.getBottom();
}
public int getWidth() {
return mImageTileSet.getWidth();
}
public int getHeight() {
return mImageTileSet.getHeight();
}
/** @return the height of the visible area of the scrolling page, in pixels */
public int getPageHeight() {
return mSession.getPageHeight();
}
@Override
public String toString() {
return "LongScreenshot{w=" + mImageTileSet.getWidth()
+ ", h=" + mImageTileSet.getHeight() + "}";
}
public Drawable getDrawable() {
return mImageTileSet.getDrawable();
}
}
ScrollCaptureController(Context context, Executor bgExecutor, IWindowManager wms) {
mContext = context;
mUiExecutor = uiExecutor;
mBgExecutor = bgExecutor;
mImageExporter = exporter;
mImageTileSet = new ImageTileSet(context.getMainThreadHandler());
mClient = new ScrollCaptureClient(mContext, wms);
}
/**
* Run scroll capture!
* Run scroll capture. Performs a batch capture, collecting image tiles.
*
* @param connection connection to the remote window to be used
* @param callback request callback to report back to the service
* @param response a scroll capture response from a previous request which is
* {@link ScrollCaptureResponse#isConnected() connected}.
* @return a future ImageTile set containing the result
*/
public void start(Connection connection, ScrollCaptureCallback callback) {
mCaptureTime = ZonedDateTime.now();
mRequestId = UUID.randomUUID();
mCaptureCallback = callback;
ListenableFuture<LongScreenshot> run(ScrollCaptureResponse response) {
Log.d(TAG, "run: " + response);
return CallbackToFutureAdapter.getFuture(completer -> {
Log.d(TAG, "getFuture(ImageTileSet) ");
mCaptureCompleter = completer;
mBgExecutor.execute(() -> {
Log.d(TAG, "bgExecutor.execute");
float maxPages = Settings.Secure.getFloat(mContext.getContentResolver(),
SETTING_KEY_MAX_PAGES, MAX_PAGES_DEFAULT);
Log.d(TAG, "client start, maxPages=" + maxPages);
mSessionFuture = mClient.start(response, maxPages);
mSessionFuture.addListener(this::onStartComplete, mContext.getMainExecutor());
});
return "<batch scroll capture>";
});
}
float maxPages = Settings.Secure.getFloat(mContext.getContentResolver(),
SETTING_KEY_MAX_PAGES, MAX_PAGES_DEFAULT);
connection.start(this::startCapture, maxPages);
private void onStartComplete() {
try {
mSession = mSessionFuture.get();
Log.d(TAG, "got session " + mSession);
requestNextTile(0);
} catch (InterruptedException | ExecutionException e) {
// Failure to start, propagate to caller
Log.d(TAG, "session start failed!");
mCaptureCompleter.setException(e);
}
}
private void requestNextTile(int topPx) {
Log.d(TAG, "requestNextTile: " + topPx);
mTileFuture = mSession.requestTile(topPx);
mTileFuture.addListener(() -> {
try {
Log.d(TAG, "onCaptureResult");
onCaptureResult(mTileFuture.get());
} catch (InterruptedException | ExecutionException e) {
Log.e(TAG, "requestTile failed!", e);
mCaptureCompleter.setException(e);
}
}, mContext.getMainExecutor());
}
private void onCaptureResult(CaptureResult result) {
@@ -146,49 +247,24 @@ public class ScrollCaptureController {
}
if (finish) {
Session session = mSession;
mSession = null;
Log.d(TAG, "Stop.");
mUiExecutor.execute(() -> afterCaptureComplete(session));
finishCapture();
return;
}
int nextTop = (mScrollingUp)
? result.captured.top - mSession.getTileHeight() : result.captured.bottom;
Log.d(TAG, "requestTile: " + nextTop);
mSession.requestTile(nextTop, /* consumer */ this::onCaptureResult);
requestNextTile(nextTop);
}
private void startCapture(Session session) {
mSession = session;
session.requestTile(0, this::onCaptureResult);
private void finishCapture() {
Log.d(TAG, "finishCapture()");
mEndFuture = mSession.end();
mEndFuture.addListener(() -> {
Log.d(TAG, "endCapture completed");
// Provide result to caller and complete the top-level future
// Caller is responsible for releasing this resource (ImageReader/HardwareBuffers)
mCaptureCompleter.set(new LongScreenshot(mSession, mImageTileSet));
}, mContext.getMainExecutor());
}
@UiThread
void afterCaptureComplete(Session session) {
Log.d(TAG, "afterCaptureComplete");
if (mImageTileSet.isEmpty()) {
mCaptureCallback.onError();
} else {
mCaptureCallback.onComplete(mImageTileSet, session.getPageHeight());
}
}
/**
* Callback for image capture completion or error.
*/
public interface ScrollCaptureCallback {
void onComplete(ImageTileSet imageTileSet, int pageHeight);
void onError();
}
/**
* Callback for image export completion or error.
*/
public interface ExportCallback {
void onExportComplete(Uri outputUri);
void onError();
}
}

View File

@@ -24,6 +24,7 @@ import android.graphics.Paint;
import android.graphics.RecordingCanvas;
import android.graphics.Rect;
import android.graphics.RenderNode;
import android.os.CancellationSignal;
import android.os.ICancellationSignal;
import android.os.RemoteException;
import android.view.IScrollCaptureCallbacks;
@@ -37,19 +38,15 @@ import android.view.Surface;
class FakeScrollCaptureConnection extends IScrollCaptureConnection.Stub {
private final int[] mColors = {Color.RED, Color.GREEN, Color.BLUE};
private IScrollCaptureCallbacks mCallbacks;
private Surface mSurface;
private Paint mPaint;
private int mNextColor;
private HwuiContext mHwuiContext;
FakeScrollCaptureConnection(IScrollCaptureCallbacks cb) {
mCallbacks = cb;
}
private CancellationSignal mCancellationSignal;
@Override
public ICancellationSignal startCapture(Surface surface) {
mSurface = surface;
mHwuiContext = new HwuiContext(false, surface);
public ICancellationSignal startCapture(Surface surface, IScrollCaptureCallbacks callbacks) {
mCallbacks = callbacks;
mHwuiContext = new HwuiContext(surface);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.FILL);
try {
@@ -57,7 +54,9 @@ class FakeScrollCaptureConnection extends IScrollCaptureConnection.Stub {
} catch (RemoteException e) {
e.rethrowAsRuntimeException();
}
return null;
ICancellationSignal signal = CancellationSignal.createTransport();
mCancellationSignal = CancellationSignal.fromTransport(signal);
return signal;
}
@Override
@@ -72,7 +71,9 @@ class FakeScrollCaptureConnection extends IScrollCaptureConnection.Stub {
} catch (RemoteException e) {
e.rethrowAsRuntimeException();
}
return null;
ICancellationSignal signal = CancellationSignal.createTransport();
mCancellationSignal = CancellationSignal.fromTransport(signal);
return signal;
}
@Override
@@ -83,15 +84,15 @@ class FakeScrollCaptureConnection extends IScrollCaptureConnection.Stub {
e.rethrowAsRuntimeException();
} finally {
mHwuiContext.destroy();
mSurface = null;
mCallbacks = null;
}
return null;
ICancellationSignal signal = CancellationSignal.createTransport();
mCancellationSignal = CancellationSignal.fromTransport(signal);
return signal;
}
@Override
public void close() throws RemoteException {
}
// From android.view.Surface, but issues render requests synchronously with waitForPresent(true)
@@ -99,21 +100,16 @@ class FakeScrollCaptureConnection extends IScrollCaptureConnection.Stub {
private final RenderNode mRenderNode;
private final HardwareRenderer mHardwareRenderer;
private RecordingCanvas mCanvas;
private final boolean mIsWideColorGamut;
HwuiContext(boolean isWideColorGamut, Surface surface) {
HwuiContext(Surface surface) {
mRenderNode = RenderNode.create("HwuiCanvas", null);
mRenderNode.setClipToBounds(false);
mRenderNode.setForceDarkAllowed(false);
mIsWideColorGamut = isWideColorGamut;
mHardwareRenderer = new HardwareRenderer();
mHardwareRenderer.setContentRoot(mRenderNode);
mHardwareRenderer.setSurface(surface, true);
mHardwareRenderer.setColorMode(
isWideColorGamut
? ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
: ActivityInfo.COLOR_MODE_DEFAULT);
mHardwareRenderer.setColorMode(ActivityInfo.COLOR_MODE_DEFAULT);
mHardwareRenderer.setLightSourceAlpha(0.0f, 0.0f);
mHardwareRenderer.setLightSourceGeometry(0.0f, 0.0f, 0.0f, 0.0f);
}
@@ -142,9 +138,5 @@ class FakeScrollCaptureConnection extends IScrollCaptureConnection.Stub {
void destroy() {
mHardwareRenderer.destroy();
}
boolean isWideColorGamut() {
return mIsWideColorGamut;
}
}
}

View File

@@ -16,15 +16,15 @@
package com.android.systemui.screenshot;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static java.util.Objects.requireNonNull;
@@ -34,7 +34,7 @@ import android.hardware.display.DisplayManager;
import android.os.RemoteException;
import android.testing.AndroidTestingRunner;
import android.view.Display;
import android.view.IScrollCaptureCallbacks;
import android.view.IScrollCaptureResponseListener;
import android.view.IWindowManager;
import android.view.ScrollCaptureResponse;
@@ -43,30 +43,29 @@ import androidx.test.platform.app.InstrumentationRegistry;
import com.android.systemui.SysuiTestCase;
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 org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.mockito.stubbing.Answer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class ScrollCaptureClientTest extends SysuiTestCase {
private static final float MAX_PAGES = 3f;
private static final float MAX_PAGES = 3.0f;
private Context mContext;
private IWindowManager mWm;
@Spy private TestableConsumer<Session> mSessionConsumer;
@Spy private TestableConsumer<Connection> mConnectionConsumer;
@Spy private TestableConsumer<CaptureResult> mResultConsumer;
@Mock private Runnable mRunnable;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
@@ -79,46 +78,50 @@ public class ScrollCaptureClientTest extends SysuiTestCase {
}
@Test
public void testBasicClientFlow() throws RemoteException {
public void testDetectAndConnect()
throws RemoteException, InterruptedException, ExecutionException, TimeoutException {
doAnswer((Answer<Void>) invocation -> {
IScrollCaptureCallbacks cb = invocation.getArgument(3);
cb.onScrollCaptureResponse(new ScrollCaptureResponse.Builder()
IScrollCaptureResponseListener listener = invocation.getArgument(3);
listener.onScrollCaptureResponse(new ScrollCaptureResponse.Builder()
.setBoundsInWindow(new Rect(0, 0, 100, 100))
.setWindowBounds(new Rect(0, 0, 100, 100))
.setConnection(new FakeScrollCaptureConnection(cb))
.setConnection(new FakeScrollCaptureConnection())
.build());
return null;
}).when(mWm).requestScrollCapture(/* displayId */ anyInt(), /* token */ isNull(),
/* taskId */ anyInt(), any(IScrollCaptureCallbacks.class));
/* taskId */ anyInt(), any(IScrollCaptureResponseListener.class));
// Create client
ScrollCaptureClient client = new ScrollCaptureClient(mContext, mWm);
client.request(Display.DEFAULT_DISPLAY, mConnectionConsumer);
verify(mConnectionConsumer, timeout(100)).accept(any(Connection.class));
// Request scroll capture
ListenableFuture<ScrollCaptureResponse> requestFuture =
client.request(Display.DEFAULT_DISPLAY);
assertNotNull(requestFuture.get(100, TimeUnit.MILLISECONDS));
Connection conn = mConnectionConsumer.getValue();
ScrollCaptureResponse response = requestFuture.get();
assertTrue(response.isConnected());
conn.start(mSessionConsumer, MAX_PAGES);
verify(mSessionConsumer, timeout(100)).accept(any(Session.class));
// Start a session
ListenableFuture<Session> startFuture = client.start(response, MAX_PAGES);
assertNotNull(startFuture.get(100, TimeUnit.MILLISECONDS));
Session session = mSessionConsumer.getValue();
Session session = startFuture.get();
Rect request = new Rect(0, 0, session.getPageWidth(), session.getTileHeight());
session.requestTile(0, mResultConsumer);
verify(mResultConsumer, timeout(100)).accept(any(CaptureResult.class));
// Request a tile
ListenableFuture<CaptureResult> tileFuture = session.requestTile(0);
assertNotNull(tileFuture.get(100, TimeUnit.MILLISECONDS));
CaptureResult result = mResultConsumer.getValue();
assertThat(result.requested).isEqualTo(request);
assertThat(result.captured).isEqualTo(result.requested);
assertThat(result.image).isNotNull();
CaptureResult result = tileFuture.get();
assertEquals(request, result.requested);
assertEquals(result.requested, result.captured);
assertNotNull(result.image);
session.end(mRunnable);
verify(mRunnable, timeout(100)).run();
// TODO verify image
// TODO test threading
// TODO test failures
// End the session
ListenableFuture<Void> endFuture = session.end();
CountDownLatch latch = new CountDownLatch(1);
endFuture.addListener(latch::countDown, Runnable::run);
assertTrue(latch.await(100, TimeUnit.MILLISECONDS));
}
}

View File

@@ -16,15 +16,16 @@
package com.android.systemui.screenshot;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.content.Intent;
import android.graphics.Rect;
import android.os.RemoteException;
import android.testing.AndroidTestingRunner;
import android.util.Log;
import android.view.Display;
import android.view.IScrollCaptureCallbacks;
import android.view.IScrollCaptureResponseListener;
import android.view.IWindowManager;
import android.view.ScrollCaptureResponse;
import android.view.WindowManagerGlobal;
@@ -45,8 +46,9 @@ import java.util.concurrent.TimeUnit;
*/
@RunWith(AndroidTestingRunner.class)
@SmallTest
public class ScrollCaptureTest extends SysuiTestCase {
private static final String TAG = "ScrollCaptureTest";
public class ScrollCaptureFrameworkSmokeTest extends SysuiTestCase {
private static final String TAG = "ScrollCaptureFrameworkSmokeTest";
private volatile ScrollCaptureResponse mResponse;
/**
* Verifies that a request traverses from SystemUI, to WindowManager and to the app process and
@@ -64,34 +66,23 @@ public class ScrollCaptureTest extends SysuiTestCase {
final CountDownLatch latch = new CountDownLatch(1);
try {
wms.requestScrollCapture(Display.DEFAULT_DISPLAY, null, -1,
new IScrollCaptureCallbacks.Stub() {
new IScrollCaptureResponseListener.Stub() {
@Override
public void onScrollCaptureResponse(ScrollCaptureResponse response)
public void onScrollCaptureResponse(
ScrollCaptureResponse response)
throws RemoteException {
Log.d(TAG, "onScrollCaptureResponse: " + response);
mResponse = response;
latch.countDown();
}
@Override
public void onCaptureStarted() {
}
@Override
public void onImageRequestCompleted(int i, Rect rect)
throws RemoteException {
}
@Override
public void onCaptureEnded() throws RemoteException {
}
});
} catch (RemoteException e) {
Log.e(TAG, "request failed", e);
fail("caught remote exception " + e);
}
latch.await(1000, TimeUnit.MILLISECONDS);
assertNotNull(mResponse);
assertTrue(mResponse.isConnected());
assertTrue(mResponse.getWindowTitle().contains(ScrollViewActivity.class.getSimpleName()));
}
}

View File

@@ -231,7 +231,7 @@ import android.view.IOnKeyguardExitResult;
import android.view.IPinnedTaskListener;
import android.view.IRecentsAnimationRunner;
import android.view.IRotationWatcher;
import android.view.IScrollCaptureCallbacks;
import android.view.IScrollCaptureResponseListener;
import android.view.ISystemGestureExclusionListener;
import android.view.IWallpaperVisibilityListener;
import android.view.IWindow;
@@ -7145,10 +7145,10 @@ public class WindowManagerService extends IWindowManager.Stub
* @param displayId the display for the request
* @param behindClient token for a window, used to filter the search to windows behind it
* @param taskId specifies the id of a task the result must belong to or -1 to match any task
* @param callbacks to receive responses
* @param listener to receive the response
*/
public void requestScrollCapture(int displayId, @Nullable IBinder behindClient, int taskId,
IScrollCaptureCallbacks callbacks) {
IScrollCaptureResponseListener listener) {
if (!checkCallingPermission(READ_FRAME_BUFFER, "requestScrollCapture()")) {
throw new SecurityException("Requires READ_FRAME_BUFFER permission");
}
@@ -7161,7 +7161,7 @@ public class WindowManagerService extends IWindowManager.Stub
ProtoLog.e(WM_ERROR,
"Invalid displayId for requestScrollCapture: %d", displayId);
responseBuilder.setDescription(String.format("bad displayId: %d", displayId));
callbacks.onScrollCaptureResponse(responseBuilder.build());
listener.onScrollCaptureResponse(responseBuilder.build());
return;
}
WindowState topWindow = null;
@@ -7171,19 +7171,19 @@ public class WindowManagerService extends IWindowManager.Stub
WindowState targetWindow = dc.findScrollCaptureTargetWindow(topWindow, taskId);
if (targetWindow == null) {
responseBuilder.setDescription("findScrollCaptureTargetWindow returned null");
callbacks.onScrollCaptureResponse(responseBuilder.build());
listener.onScrollCaptureResponse(responseBuilder.build());
return;
}
try {
// Forward to the window for handling, which will respond using the callback.
targetWindow.mClient.requestScrollCapture(callbacks);
targetWindow.mClient.requestScrollCapture(listener);
} catch (RemoteException e) {
ProtoLog.w(WM_ERROR,
"requestScrollCapture: caught exception dispatching to window."
+ "token=%s", targetWindow.mClient.asBinder());
responseBuilder.setWindowTitle(targetWindow.getName());
responseBuilder.setDescription(String.format("caught exception: %s", e));
callbacks.onScrollCaptureResponse(responseBuilder.build());
listener.onScrollCaptureResponse(responseBuilder.build());
}
}
} catch (RemoteException e) {

View File

@@ -22,10 +22,11 @@ import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.MergedConfiguration;
import android.view.DragEvent;
import android.view.IScrollCaptureCallbacks;
import android.view.IScrollCaptureResponseListener;
import android.view.IWindow;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.ScrollCaptureResponse;
import android.window.ClientWindowFrames;
import com.android.internal.os.IResultReceiver;
@@ -116,7 +117,15 @@ public class TestIWindow extends IWindow.Stub {
}
@Override
public void requestScrollCapture(IScrollCaptureCallbacks callbacks) throws RemoteException {
public void requestScrollCapture(IScrollCaptureResponseListener listener)
throws RemoteException {
try {
listener.onScrollCaptureResponse(
new ScrollCaptureResponse.Builder().setDescription("Not Implemented").build());
} catch (RemoteException ex) {
// ignore
}
}
@Override