diff options
Diffstat (limited to 'core/java/android/view/ScrollCaptureConnection.java')
| -rw-r--r-- | core/java/android/view/ScrollCaptureConnection.java | 373 |
1 files changed, 192 insertions, 181 deletions
diff --git a/core/java/android/view/ScrollCaptureConnection.java b/core/java/android/view/ScrollCaptureConnection.java index 0e6cdd1dbec5..3456e016c42c 100644 --- a/core/java/android/view/ScrollCaptureConnection.java +++ b/core/java/android/view/ScrollCaptureConnection.java @@ -18,18 +18,23 @@ package android.view; import static java.util.Objects.requireNonNull; +import android.annotation.BinderThread; import android.annotation.NonNull; import android.annotation.UiThread; -import android.annotation.WorkerThread; import android.graphics.Point; import android.graphics.Rect; -import android.os.Handler; +import android.os.CancellationSignal; +import android.os.ICancellationSignal; import android.os.RemoteException; +import android.os.Trace; import android.util.CloseGuard; +import android.util.Log; import com.android.internal.annotations.VisibleForTesting; -import java.util.concurrent.atomic.AtomicBoolean; +import java.lang.ref.WeakReference; +import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * Mediator between a selected scroll capture target view and a remote process. @@ -41,270 +46,276 @@ import java.util.concurrent.atomic.AtomicBoolean; public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub { private static final String TAG = "ScrollCaptureConnection"; - private static final int DEFAULT_TIMEOUT = 1000; - - private final Handler mHandler; - private ScrollCaptureTarget mSelectedTarget; - private int mTimeoutMillis = DEFAULT_TIMEOUT; - - protected Surface mSurface; - private IScrollCaptureCallbacks mCallbacks; + private final Object mLock = new Object(); private final Rect mScrollBounds; private final Point mPositionInWindow; private final CloseGuard mCloseGuard; + private final Executor mUiThread; + + private ScrollCaptureCallback mLocal; + private IScrollCaptureCallbacks mRemote; - // The current session instance in use by the callback. private ScrollCaptureSession mSession; - // Helps manage timeout callbacks registered to handler and aids testing. - private DelayedAction mTimeoutAction; + private CancellationSignal mCancellation; + + private volatile boolean mStarted; + private volatile boolean mConnected; /** * Constructs a ScrollCaptureConnection. * * @param selectedTarget the target the client is controlling - * @param callbacks the callbacks to reply to system requests + * @param remote the callbacks to reply to system requests * * @hide */ public ScrollCaptureConnection( + @NonNull Executor uiThread, @NonNull ScrollCaptureTarget selectedTarget, - @NonNull IScrollCaptureCallbacks callbacks) { + @NonNull IScrollCaptureCallbacks remote) { + mUiThread = requireNonNull(uiThread, "<uiThread> must non-null"); requireNonNull(selectedTarget, "<selectedTarget> must non-null"); - requireNonNull(callbacks, "<callbacks> must non-null"); - final Rect scrollBounds = requireNonNull(selectedTarget.getScrollBounds(), + mRemote = requireNonNull(remote, "<callbacks> must non-null"); + mScrollBounds = requireNonNull(Rect.copyOrNull(selectedTarget.getScrollBounds()), "target.getScrollBounds() must be non-null to construct a client"); - mSelectedTarget = selectedTarget; - mHandler = selectedTarget.getContainingView().getHandler(); - mScrollBounds = new Rect(scrollBounds); + mLocal = selectedTarget.getCallback(); mPositionInWindow = new Point(selectedTarget.getPositionInWindow()); - mCallbacks = callbacks; mCloseGuard = new CloseGuard(); mCloseGuard.open("close"); - - selectedTarget.getContainingView().addOnAttachStateChangeListener( - new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View v) { - - } - - @Override - public void onViewDetachedFromWindow(View v) { - selectedTarget.getContainingView().removeOnAttachStateChangeListener(this); - endCapture(); - } - }); - } - - @VisibleForTesting - public void setTimeoutMillis(int timeoutMillis) { - mTimeoutMillis = timeoutMillis; + mConnected = true; } - @VisibleForTesting - public DelayedAction getTimeoutAction() { - return mTimeoutAction; - } - - private void checkConnected() { - if (mSelectedTarget == null || mCallbacks == null) { - throw new IllegalStateException("This client has been disconnected."); + @BinderThread + @Override + public ICancellationSignal startCapture(Surface surface) throws RemoteException { + checkConnected(); + if (!surface.isValid()) { + throw new RemoteException(new IllegalArgumentException("surface must be valid")); } - } - private void checkStarted() { - if (mSession == null) { - throw new IllegalStateException("Capture session has not been started!"); - } - } + ICancellationSignal cancellation = CancellationSignal.createTransport(); + mCancellation = CancellationSignal.fromTransport(cancellation); + mSession = new ScrollCaptureSession(surface, mScrollBounds, mPositionInWindow); - @WorkerThread // IScrollCaptureConnection - @Override - public void startCapture(Surface surface) throws RemoteException { - checkConnected(); - mSurface = surface; - scheduleTimeout(mTimeoutMillis, this::onStartCaptureTimeout); - mSession = new ScrollCaptureSession(mSurface, mScrollBounds, mPositionInWindow, this); - mHandler.post(() -> mSelectedTarget.getCallback().onScrollCaptureStart(mSession, - this::onStartCaptureCompleted)); + Runnable listener = + SafeCallback.create(mCancellation, mUiThread, this::onStartCaptureCompleted); + // -> UiThread + mUiThread.execute(() -> mLocal.onScrollCaptureStart(mSession, mCancellation, listener)); + return cancellation; } @UiThread private void onStartCaptureCompleted() { - if (cancelTimeout()) { - mHandler.post(() -> { - try { - mCallbacks.onCaptureStarted(); - } catch (RemoteException e) { - doShutdown(); - } - }); + mStarted = true; + try { + mRemote.onCaptureStarted(); + } catch (RemoteException e) { + Log.w(TAG, "Shutting down due to error: ", e); + close(); } } - @UiThread - private void onStartCaptureTimeout() { - endCapture(); - } - @WorkerThread // IScrollCaptureConnection + @BinderThread @Override - public void requestImage(Rect requestRect) { + public ICancellationSignal requestImage(Rect requestRect) throws RemoteException { + Trace.beginSection("requestImage"); checkConnected(); checkStarted(); - scheduleTimeout(mTimeoutMillis, this::onRequestImageTimeout); - // Response is dispatched via ScrollCaptureSession, to onRequestImageCompleted - mHandler.post(() -> mSelectedTarget.getCallback().onScrollCaptureImageRequest( - mSession, new Rect(requestRect))); - } - @UiThread - void onRequestImageCompleted(long frameNumber, Rect capturedArea) { - final Rect finalCapturedArea = new Rect(capturedArea); - if (cancelTimeout()) { - mHandler.post(() -> { - try { - mCallbacks.onCaptureBufferSent(frameNumber, finalCapturedArea); - } catch (RemoteException e) { - doShutdown(); - } - }); - } + ICancellationSignal cancellation = CancellationSignal.createTransport(); + mCancellation = CancellationSignal.fromTransport(cancellation); + + Consumer<Rect> listener = + SafeCallback.create(mCancellation, mUiThread, this::onImageRequestCompleted); + // -> UiThread + mUiThread.execute(() -> mLocal.onScrollCaptureImageRequest( + mSession, mCancellation, new Rect(requestRect), listener)); + Trace.endSection(); + return cancellation; } @UiThread - private void onRequestImageTimeout() { - endCapture(); + void onImageRequestCompleted(Rect capturedArea) { + try { + mRemote.onImageRequestCompleted(0, capturedArea); + } catch (RemoteException e) { + Log.w(TAG, "Shutting down due to error: ", e); + close(); + } } - @WorkerThread // IScrollCaptureConnection + @BinderThread @Override - public void endCapture() { - if (isStarted()) { - scheduleTimeout(mTimeoutMillis, this::onEndCaptureTimeout); - mHandler.post(() -> - mSelectedTarget.getCallback().onScrollCaptureEnd(this::onEndCaptureCompleted)); - } else { - disconnect(); - } - } + public ICancellationSignal endCapture() throws RemoteException { + checkConnected(); + checkStarted(); - private boolean isStarted() { - return mCallbacks != null && mSelectedTarget != null; - } + ICancellationSignal cancellation = CancellationSignal.createTransport(); + mCancellation = CancellationSignal.fromTransport(cancellation); - @UiThread - private void onEndCaptureCompleted() { // onEndCaptureCompleted - if (cancelTimeout()) { - doShutdown(); - } + Runnable listener = + SafeCallback.create(mCancellation, mUiThread, this::onEndCaptureCompleted); + // -> UiThread + mUiThread.execute(() -> mLocal.onScrollCaptureEnd(listener)); + return cancellation; } @UiThread - private void onEndCaptureTimeout() { - doShutdown(); + private void onEndCaptureCompleted() { + synchronized (mLock) { + mStarted = false; + try { + mRemote.onCaptureEnded(); + } catch (RemoteException e) { + Log.w(TAG, "Shutting down due to error: ", e); + close(); + } + } } + @BinderThread + @Override + public void close() { + if (mStarted) { + Log.w(TAG, "close(): capture is still started?! Ending now."); - private void doShutdown() { - try { - if (mCallbacks != null) { - mCallbacks.onConnectionClosed(); - } - } catch (RemoteException e) { - // Ignore - } finally { - disconnect(); + // -> UiThread + mUiThread.execute(() -> mLocal.onScrollCaptureEnd(() -> { /* ignore */ })); + mStarted = false; } + disconnect(); } /** * Shuts down this client and releases references to dependent objects. No attempt is made * to notify the controller, use with caution! */ - public void disconnect() { - if (mSession != null) { - mSession.disconnect(); + private void disconnect() { + synchronized (mLock) { mSession = null; + mConnected = false; + mStarted = false; + mRemote = null; + mLocal = null; + mCloseGuard.close(); } + } + + public boolean isConnected() { + return mConnected; + } - mSelectedTarget = null; - mCallbacks = null; + 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) { + throw new RemoteException(new IllegalStateException("Not started!")); + } + } } /** @return a string representation of the state of this client */ public String toString() { return "ScrollCaptureConnection{" + + "connected=" + mConnected + + ", started=" + mStarted + ", session=" + mSession - + ", selectedTarget=" + mSelectedTarget - + ", clientCallbacks=" + mCallbacks + + ", remote=" + mRemote + + ", local=" + mLocal + "}"; } - private boolean cancelTimeout() { - if (mTimeoutAction != null) { - return mTimeoutAction.cancel(); - } - return false; + @VisibleForTesting + public CancellationSignal getCancellation() { + return mCancellation; } - private void scheduleTimeout(long timeoutMillis, Runnable action) { - if (mTimeoutAction != null) { - mTimeoutAction.cancel(); + protected void finalize() throws Throwable { + try { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + close(); + } finally { + super.finalize(); } - mTimeoutAction = new DelayedAction(mHandler, timeoutMillis, action); } - /** @hide */ - @VisibleForTesting - public static class DelayedAction { - private final AtomicBoolean mCompleted = new AtomicBoolean(); - private final Object mToken = new Object(); - private final Handler mHandler; - private final Runnable mAction; - - @VisibleForTesting - public DelayedAction(Handler handler, long timeoutMillis, Runnable action) { - mHandler = handler; - mAction = action; - mHandler.postDelayed(this::onTimeout, mToken, timeoutMillis); + private static class SafeCallback<T> { + private final CancellationSignal mSignal; + private final WeakReference<T> mTargetRef; + private final Executor mExecutor; + private boolean mExecuted; + + protected SafeCallback(CancellationSignal signal, Executor executor, T target) { + mSignal = signal; + mTargetRef = new WeakReference<>(target); + mExecutor = executor; } - private boolean onTimeout() { - if (mCompleted.compareAndSet(false, true)) { - mAction.run(); - return true; + // Provide the target to the consumer to invoke, forward on handler thread ONCE, + // and only if noy cancelled, and the target is still available (not collected) + protected final void maybeAccept(Consumer<T> targetConsumer) { + if (mExecuted) { + return; + } + mExecuted = true; + if (mSignal.isCanceled()) { + return; + } + T target = mTargetRef.get(); + if (target == null) { + return; } - return false; + mExecutor.execute(() -> targetConsumer.accept(target)); } - /** - * Cause the timeout action to run immediately and mark as timed out. - * - * @return true if the timeout was run, false if the timeout had already been canceled - */ - @VisibleForTesting - public boolean timeoutNow() { - return onTimeout(); + static Runnable create(CancellationSignal signal, Executor executor, Runnable target) { + return new RunnableCallback(signal, executor, target); } - /** - * Attempt to cancel the timeout action (such as after a callback is made) - * - * @return true if the timeout was canceled and will not run, false if time has expired and - * the timeout action has or will run momentarily - */ - public boolean cancel() { - if (!mCompleted.compareAndSet(false, true)) { - // Whoops, too late! - return false; - } - mHandler.removeCallbacksAndMessages(mToken); - return true; + static <T> Consumer<T> create(CancellationSignal signal, Executor executor, + Consumer<T> target) { + return new ConsumerCallback<T>(signal, executor, target); + } + } + + private static final class RunnableCallback extends SafeCallback<Runnable> implements Runnable { + RunnableCallback(CancellationSignal signal, Executor executor, Runnable target) { + super(signal, executor, target); + } + + @Override + public void run() { + maybeAccept(Runnable::run); + } + } + + private static final class ConsumerCallback<T> extends SafeCallback<Consumer<T>> + implements Consumer<T> { + ConsumerCallback(CancellationSignal signal, Executor executor, Consumer<T> target) { + super(signal, executor, target); + } + + @Override + public void accept(T value) { + maybeAccept((target) -> target.accept(value)); } } } |
