| /* |
| * 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. |
| * <p> |
| * 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, "<uiThread> must non-null"); |
| requireNonNull(selectedTarget, "<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, "<callbacks> 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, START_CAPTURE, 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<Rect> 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, REQUEST_IMAGE, 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, END_CAPTURE, mTraceId); |
| Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, SESSION, mTraceId); |
| } |
| |
| @Override |
| public void binderDied() { |
| Trace.instantForTrack(TRACE_TAG_GRAPHICS, TRACE_TRACK, "binderDied"); |
| Log.e(TAG, "Controlling process just died."); |
| close(); |
| |
| } |
| |
| @BinderThread |
| @Override |
| public 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<T> { |
| private final CancellationSignal mSignal; |
| private final Executor mExecutor; |
| private final AtomicReference<T> 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<T> 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 <T> Consumer<T> create(CancellationSignal signal, Executor executor, |
| Consumer<T> target) { |
| return new ConsumerCallback<>(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)); |
| } |
| } |
| } |