/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.view; import static android.os.Trace.TRACE_TAG_GRAPHICS; import static java.util.Objects.requireNonNull; import android.annotation.BinderThread; import android.annotation.NonNull; import android.annotation.UiThread; import android.graphics.Point; import android.graphics.Rect; import android.os.CancellationSignal; import android.os.IBinder; 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.lang.ref.Reference; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; /** * Mediator between a selected scroll capture target view and a remote process. *

* An instance is created to wrap the selected {@link ScrollCaptureCallback}. * * @hide */ public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub implements IBinder.DeathRecipient { private static final String TAG = "ScrollCaptureConnection"; private static final String TRACE_TRACK = "Scroll Capture"; private static final String START_CAPTURE = "startCapture"; private static final String REQUEST_IMAGE = "requestImage"; private static final String END_CAPTURE = "endCapture"; private static final String SESSION = "Session"; private final Object mLock = new Object(); private final Rect mScrollBounds; private final Point mPositionInWindow; private final Executor mUiThread; private final CloseGuard mCloseGuard = new CloseGuard(); private ScrollCaptureCallback mLocal; private IScrollCaptureCallbacks mRemote; private ScrollCaptureSession mSession; private CancellationSignal mCancellation; private volatile boolean mActive; private volatile boolean mConnected; private int mTraceId; /** * Constructs a ScrollCaptureConnection. * * @param uiThread an executor for the UI thread of the containing View * @param selectedTarget the target the client is controlling * * @hide */ public ScrollCaptureConnection( @NonNull Executor uiThread, @NonNull ScrollCaptureTarget selectedTarget) { mUiThread = requireNonNull(uiThread, " must non-null"); requireNonNull(selectedTarget, " 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()); } @BinderThread @Override public ICancellationSignal startCapture(@NonNull Surface surface, @NonNull IScrollCaptureCallbacks remote) throws RemoteException { mTraceId = System.identityHashCode(surface); Trace.asyncTraceForTrackBegin(TRACE_TAG_GRAPHICS, TRACE_TRACK, SESSION, mTraceId); Trace.asyncTraceForTrackBegin(TRACE_TAG_GRAPHICS, TRACE_TRACK, START_CAPTURE, mTraceId); mCloseGuard.open("ScrollCaptureConnection.close"); if (!surface.isValid()) { throw new RemoteException(new IllegalArgumentException("surface must be valid")); } mRemote = requireNonNull(remote, " must non-null"); mRemote.asBinder().linkToDeath(this, 0); mConnected = true; ICancellationSignal cancellation = CancellationSignal.createTransport(); mCancellation = CancellationSignal.fromTransport(cancellation); mSession = new ScrollCaptureSession(surface, mScrollBounds, mPositionInWindow); Runnable listener = SafeCallback.create(mCancellation, mUiThread, this::onStartCaptureCompleted); // -> UiThread mUiThread.execute(() -> mLocal.onScrollCaptureStart(mSession, mCancellation, listener)); return cancellation; } @UiThread private void onStartCaptureCompleted() { mActive = true; try { mRemote.onCaptureStarted(); } catch (RemoteException e) { Log.w(TAG, "Shutting down due to error: ", e); close(); } mCancellation = null; Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, mTraceId); } @BinderThread @Override public ICancellationSignal requestImage(Rect requestRect) throws RemoteException { Trace.asyncTraceForTrackBegin(TRACE_TAG_GRAPHICS, TRACE_TRACK, REQUEST_IMAGE, mTraceId); checkActive(); cancelPendingAction(); ICancellationSignal cancellation = CancellationSignal.createTransport(); mCancellation = CancellationSignal.fromTransport(cancellation); Consumer listener = SafeCallback.create(mCancellation, mUiThread, this::onImageRequestCompleted); // -> UiThread mUiThread.execute(() -> { if (mLocal != null) { mLocal.onScrollCaptureImageRequest( mSession, mCancellation, new Rect(requestRect), listener); } }); return cancellation; } @UiThread void onImageRequestCompleted(Rect capturedArea) { try { mRemote.onImageRequestCompleted(0, capturedArea); } catch (RemoteException e) { Log.w(TAG, "Shutting down due to error: ", e); close(); } finally { mCancellation = null; } Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, mTraceId); } @BinderThread @Override public ICancellationSignal endCapture() throws RemoteException { Trace.asyncTraceForTrackBegin(TRACE_TAG_GRAPHICS, TRACE_TRACK, END_CAPTURE, mTraceId); checkActive(); cancelPendingAction(); ICancellationSignal cancellation = CancellationSignal.createTransport(); mCancellation = CancellationSignal.fromTransport(cancellation); Runnable listener = SafeCallback.create(mCancellation, mUiThread, this::onEndCaptureCompleted); // -> UiThread mUiThread.execute(() -> { if (mLocal != null) { mLocal.onScrollCaptureEnd(listener); } }); return cancellation; } @UiThread private void onEndCaptureCompleted() { mActive = false; try { if (mRemote != null) { mRemote.onCaptureEnded(); } } catch (RemoteException e) { Log.w(TAG, "Caught exception confirming capture end!", e); } finally { mCancellation = null; close(); } Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, mTraceId); Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, mTraceId); } @Override public void binderDied() { Trace.instantForTrack(TRACE_TAG_GRAPHICS, TRACE_TRACK, "binderDied"); Log.e(TAG, "Controlling process just died."); close(); } @BinderThread @Override public synchronized void close() { Trace.instantForTrack(TRACE_TAG_GRAPHICS, TRACE_TRACK, "close"); if (mActive) { Log.w(TAG, "close(): capture session still active! Ending now."); cancelPendingAction(); final ScrollCaptureCallback callback = mLocal; mUiThread.execute(() -> callback.onScrollCaptureEnd(() -> { /* ignore */ })); mActive = false; } if (mRemote != null) { mRemote.asBinder().unlinkToDeath(this, 0); } mActive = false; mConnected = false; mSession = null; mRemote = null; mLocal = null; mCloseGuard.close(); Trace.endSection(); Reference.reachabilityFence(this); } private void cancelPendingAction() { if (mCancellation != null) { Trace.instantForTrack(TRACE_TAG_GRAPHICS, TRACE_TRACK, "CancellationSignal.cancel"); Log.w(TAG, "cancelling pending operation."); mCancellation.cancel(); mCancellation = null; } } @VisibleForTesting public boolean isConnected() { return mConnected; } @VisibleForTesting public boolean isActive() { return mActive; } private void checkActive() throws RemoteException { synchronized (mLock) { if (!mActive) { throw new RemoteException(new IllegalStateException("Not started!")); } } } /** @return a string representation of the state of this client */ public String toString() { return "ScrollCaptureConnection{" + "active=" + mActive + ", session=" + mSession + ", remote=" + mRemote + ", local=" + mLocal + "}"; } protected void finalize() throws Throwable { try { mCloseGuard.warnIfOpen(); close(); } finally { super.finalize(); } } private static class SafeCallback { private final CancellationSignal mSignal; private final Executor mExecutor; private final AtomicReference mValue; protected SafeCallback(CancellationSignal signal, Executor executor, T value) { mSignal = signal; mValue = new AtomicReference<>(value); mExecutor = executor; } // Provide the value to the consumer to accept only once. protected final void maybeAccept(Consumer consumer) { T value = mValue.getAndSet(null); if (mSignal.isCanceled()) { return; } if (value != null) { mExecutor.execute(() -> consumer.accept(value)); } } static Runnable create(CancellationSignal signal, Executor executor, Runnable target) { return new RunnableCallback(signal, executor, target); } static Consumer create(CancellationSignal signal, Executor executor, Consumer target) { return new ConsumerCallback<>(signal, executor, target); } } private static final class RunnableCallback extends SafeCallback 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 extends SafeCallback> implements Consumer { ConsumerCallback(CancellationSignal signal, Executor executor, Consumer target) { super(signal, executor, target); } @Override public void accept(T value) { maybeAccept((target) -> target.accept(value)); } } }