Merge "Splits up internal binder api for scroll capture" into sc-dev
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user