1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.view;
18 
19 import static java.util.Objects.requireNonNull;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.annotation.UiThread;
24 import android.annotation.WorkerThread;
25 import android.graphics.Point;
26 import android.graphics.Rect;
27 import android.os.Handler;
28 import android.os.RemoteException;
29 import android.util.CloseGuard;
30 
31 import com.android.internal.annotations.VisibleForTesting;
32 
33 import java.util.concurrent.atomic.AtomicBoolean;
34 
35 /**
36  * A client of the system providing Scroll Capture capability on behalf of a Window.
37  * <p>
38  * An instance is created to wrap the selected {@link ScrollCaptureCallback}.
39  *
40  * @hide
41  */
42 public class ScrollCaptureClient extends IScrollCaptureClient.Stub {
43 
44     private static final String TAG = "ScrollCaptureClient";
45     private static final int DEFAULT_TIMEOUT = 1000;
46 
47     private final Handler mHandler;
48     private ScrollCaptureTarget mSelectedTarget;
49     private int mTimeoutMillis = DEFAULT_TIMEOUT;
50 
51     protected Surface mSurface;
52     private IScrollCaptureController mController;
53 
54     private final Rect mScrollBounds;
55     private final Point mPositionInWindow;
56     private final CloseGuard mCloseGuard;
57 
58     // The current session instance in use by the callback.
59     private ScrollCaptureSession mSession;
60 
61     // Helps manage timeout callbacks registered to handler and aids testing.
62     private DelayedAction mTimeoutAction;
63 
64     /**
65      * Constructs a ScrollCaptureClient.
66      *
67      * @param selectedTarget  the target the client is controlling
68      * @param controller the callbacks to reply to system requests
69      *
70      * @hide
71      */
ScrollCaptureClient( @onNull ScrollCaptureTarget selectedTarget, @NonNull IScrollCaptureController controller)72     public ScrollCaptureClient(
73             @NonNull ScrollCaptureTarget selectedTarget,
74             @NonNull IScrollCaptureController controller) {
75         requireNonNull(selectedTarget, "<selectedTarget> must non-null");
76         requireNonNull(controller, "<controller> must non-null");
77         final Rect scrollBounds = requireNonNull(selectedTarget.getScrollBounds(),
78                 "target.getScrollBounds() must be non-null to construct a client");
79 
80         mSelectedTarget = selectedTarget;
81         mHandler = selectedTarget.getContainingView().getHandler();
82         mScrollBounds = new Rect(scrollBounds);
83         mPositionInWindow = new Point(selectedTarget.getPositionInWindow());
84 
85         mController = controller;
86         mCloseGuard = new CloseGuard();
87         mCloseGuard.open("close");
88 
89         selectedTarget.getContainingView().addOnAttachStateChangeListener(
90                 new View.OnAttachStateChangeListener() {
91                     @Override
92                     public void onViewAttachedToWindow(View v) {
93 
94                     }
95 
96                     @Override
97                     public void onViewDetachedFromWindow(View v) {
98                         selectedTarget.getContainingView().removeOnAttachStateChangeListener(this);
99                         endCapture();
100                     }
101                 });
102     }
103 
104     @VisibleForTesting
setTimeoutMillis(int timeoutMillis)105     public void setTimeoutMillis(int timeoutMillis) {
106         mTimeoutMillis = timeoutMillis;
107     }
108 
109     @Nullable
110     @VisibleForTesting
getTimeoutAction()111     public DelayedAction getTimeoutAction() {
112         return mTimeoutAction;
113     }
114 
checkConnected()115     private void checkConnected() {
116         if (mSelectedTarget == null || mController == null) {
117             throw new IllegalStateException("This client has been disconnected.");
118         }
119     }
120 
checkStarted()121     private void checkStarted() {
122         if (mSession == null) {
123             throw new IllegalStateException("Capture session has not been started!");
124         }
125     }
126 
127     @WorkerThread // IScrollCaptureClient
128     @Override
startCapture(Surface surface)129     public void startCapture(Surface surface) throws RemoteException {
130         checkConnected();
131         mSurface = surface;
132         scheduleTimeout(mTimeoutMillis, this::onStartCaptureTimeout);
133         mSession = new ScrollCaptureSession(mSurface, mScrollBounds, mPositionInWindow, this);
134         mHandler.post(() -> mSelectedTarget.getCallback().onScrollCaptureStart(mSession,
135                 this::onStartCaptureCompleted));
136     }
137 
138     @UiThread
onStartCaptureCompleted()139     private void onStartCaptureCompleted() {
140         if (cancelTimeout()) {
141             mHandler.post(() -> {
142                 try {
143                     mController.onCaptureStarted();
144                 } catch (RemoteException e) {
145                     doShutdown();
146                 }
147             });
148         }
149     }
150 
151     @UiThread
onStartCaptureTimeout()152     private void onStartCaptureTimeout() {
153         endCapture();
154     }
155 
156     @WorkerThread // IScrollCaptureClient
157     @Override
requestImage(Rect requestRect)158     public void requestImage(Rect requestRect) {
159         checkConnected();
160         checkStarted();
161         scheduleTimeout(mTimeoutMillis, this::onRequestImageTimeout);
162         // Response is dispatched via ScrollCaptureSession, to onRequestImageCompleted
163         mHandler.post(() -> mSelectedTarget.getCallback().onScrollCaptureImageRequest(
164                 mSession, new Rect(requestRect)));
165     }
166 
167     @UiThread
onRequestImageCompleted(long frameNumber, Rect capturedArea)168     void onRequestImageCompleted(long frameNumber, Rect capturedArea) {
169         final Rect finalCapturedArea = new Rect(capturedArea);
170         if (cancelTimeout()) {
171             mHandler.post(() -> {
172                 try {
173                     mController.onCaptureBufferSent(frameNumber, finalCapturedArea);
174                 } catch (RemoteException e) {
175                     doShutdown();
176                 }
177             });
178         }
179     }
180 
181     @UiThread
onRequestImageTimeout()182     private void onRequestImageTimeout() {
183         endCapture();
184     }
185 
186     @WorkerThread // IScrollCaptureClient
187     @Override
endCapture()188     public void endCapture() {
189         if (isStarted()) {
190             scheduleTimeout(mTimeoutMillis, this::onEndCaptureTimeout);
191             mHandler.post(() ->
192                     mSelectedTarget.getCallback().onScrollCaptureEnd(this::onEndCaptureCompleted));
193         } else {
194             disconnect();
195         }
196     }
197 
isStarted()198     private boolean isStarted() {
199         return mController != null && mSelectedTarget != null;
200     }
201 
202     @UiThread
onEndCaptureCompleted()203     private void onEndCaptureCompleted() { // onEndCaptureCompleted
204         if (cancelTimeout()) {
205             doShutdown();
206         }
207     }
208 
209     @UiThread
onEndCaptureTimeout()210     private void onEndCaptureTimeout() {
211         doShutdown();
212     }
213 
214 
doShutdown()215     private void doShutdown() {
216         try {
217             if (mController != null) {
218                 mController.onConnectionClosed();
219             }
220         } catch (RemoteException e) {
221             // Ignore
222         } finally {
223             disconnect();
224         }
225     }
226 
227     /**
228      * Shuts down this client and releases references to dependent objects. No attempt is made
229      * to notify the controller, use with caution!
230      */
disconnect()231     public void disconnect() {
232         if (mSession != null) {
233             mSession.disconnect();
234             mSession = null;
235         }
236 
237         mSelectedTarget = null;
238         mController = null;
239     }
240 
241     /** @return a string representation of the state of this client */
toString()242     public String toString() {
243         return "ScrollCaptureClient{"
244                 + ", session=" + mSession
245                 + ", selectedTarget=" + mSelectedTarget
246                 + ", clientCallbacks=" + mController
247                 + "}";
248     }
249 
cancelTimeout()250     private boolean cancelTimeout() {
251         if (mTimeoutAction != null) {
252             return mTimeoutAction.cancel();
253         }
254         return false;
255     }
256 
scheduleTimeout(long timeoutMillis, Runnable action)257     private void scheduleTimeout(long timeoutMillis, Runnable action) {
258         if (mTimeoutAction != null) {
259             mTimeoutAction.cancel();
260         }
261         mTimeoutAction = new DelayedAction(mHandler, timeoutMillis, action);
262     }
263 
264     /** @hide */
265     @VisibleForTesting
266     public static class DelayedAction {
267         private final AtomicBoolean mCompleted = new AtomicBoolean();
268         private final Object mToken = new Object();
269         private final Handler mHandler;
270         private final Runnable mAction;
271 
272         @VisibleForTesting
DelayedAction(Handler handler, long timeoutMillis, Runnable action)273         public DelayedAction(Handler handler, long timeoutMillis, Runnable action) {
274             mHandler = handler;
275             mAction = action;
276             mHandler.postDelayed(this::onTimeout, mToken, timeoutMillis);
277         }
278 
onTimeout()279         private boolean onTimeout() {
280             if (mCompleted.compareAndSet(false, true)) {
281                 mAction.run();
282                 return true;
283             }
284             return false;
285         }
286 
287         /**
288          * Cause the timeout action to run immediately and mark as timed out.
289          *
290          * @return true if the timeout was run, false if the timeout had already been canceled
291          */
292         @VisibleForTesting
timeoutNow()293         public boolean timeoutNow() {
294             return onTimeout();
295         }
296 
297         /**
298          * Attempt to cancel the timeout action (such as after a callback is made)
299          *
300          * @return true if the timeout was canceled and will not run, false if time has expired and
301          * the timeout action has or will run momentarily
302          */
cancel()303         public boolean cancel() {
304             if (!mCompleted.compareAndSet(false, true)) {
305                 // Whoops, too late!
306                 return false;
307             }
308             mHandler.removeCallbacksAndMessages(mToken);
309             return true;
310         }
311     }
312 }
313