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