1 package com.android.internal.util; 2 3 import static android.content.Intent.ACTION_USER_SWITCHED; 4 import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER; 5 6 import android.annotation.NonNull; 7 import android.annotation.Nullable; 8 import android.content.BroadcastReceiver; 9 import android.content.ComponentName; 10 import android.content.Context; 11 import android.content.Intent; 12 import android.content.IntentFilter; 13 import android.content.ServiceConnection; 14 import android.graphics.Insets; 15 import android.graphics.Rect; 16 import android.net.Uri; 17 import android.os.Bundle; 18 import android.os.Handler; 19 import android.os.IBinder; 20 import android.os.Message; 21 import android.os.Messenger; 22 import android.os.Parcel; 23 import android.os.Parcelable; 24 import android.os.RemoteException; 25 import android.os.UserHandle; 26 import android.util.Log; 27 import android.view.WindowManager; 28 29 import java.util.function.Consumer; 30 31 public class ScreenshotHelper { 32 33 public static final int SCREENSHOT_MSG_URI = 1; 34 public static final int SCREENSHOT_MSG_PROCESS_COMPLETE = 2; 35 36 /** 37 * Describes a screenshot request (to make it easier to pass data through to the handler). 38 */ 39 public static class ScreenshotRequest implements Parcelable { 40 private int mSource; 41 private boolean mHasStatusBar; 42 private boolean mHasNavBar; 43 private Bundle mBitmapBundle; 44 private Rect mBoundsInScreen; 45 private Insets mInsets; 46 private int mTaskId; 47 private int mUserId; 48 private ComponentName mTopComponent; 49 ScreenshotRequest(int source, boolean hasStatus, boolean hasNav)50 ScreenshotRequest(int source, boolean hasStatus, boolean hasNav) { 51 mSource = source; 52 mHasStatusBar = hasStatus; 53 mHasNavBar = hasNav; 54 } 55 ScreenshotRequest(int source, Bundle bitmapBundle, Rect boundsInScreen, Insets insets, int taskId, int userId, ComponentName topComponent)56 ScreenshotRequest(int source, Bundle bitmapBundle, Rect boundsInScreen, Insets insets, 57 int taskId, int userId, ComponentName topComponent) { 58 mSource = source; 59 mBitmapBundle = bitmapBundle; 60 mBoundsInScreen = boundsInScreen; 61 mInsets = insets; 62 mTaskId = taskId; 63 mUserId = userId; 64 mTopComponent = topComponent; 65 } 66 ScreenshotRequest(Parcel in)67 ScreenshotRequest(Parcel in) { 68 mSource = in.readInt(); 69 mHasStatusBar = in.readBoolean(); 70 mHasNavBar = in.readBoolean(); 71 72 if (in.readInt() == 1) { 73 mBitmapBundle = in.readBundle(getClass().getClassLoader()); 74 mBoundsInScreen = in.readParcelable(Rect.class.getClassLoader()); 75 mInsets = in.readParcelable(Insets.class.getClassLoader()); 76 mTaskId = in.readInt(); 77 mUserId = in.readInt(); 78 mTopComponent = in.readParcelable(ComponentName.class.getClassLoader()); 79 } 80 } 81 getSource()82 public int getSource() { 83 return mSource; 84 } 85 getHasStatusBar()86 public boolean getHasStatusBar() { 87 return mHasStatusBar; 88 } 89 getHasNavBar()90 public boolean getHasNavBar() { 91 return mHasNavBar; 92 } 93 getBitmapBundle()94 public Bundle getBitmapBundle() { 95 return mBitmapBundle; 96 } 97 getBoundsInScreen()98 public Rect getBoundsInScreen() { 99 return mBoundsInScreen; 100 } 101 getInsets()102 public Insets getInsets() { 103 return mInsets; 104 } 105 getTaskId()106 public int getTaskId() { 107 return mTaskId; 108 } 109 110 getUserId()111 public int getUserId() { 112 return mUserId; 113 } 114 getTopComponent()115 public ComponentName getTopComponent() { 116 return mTopComponent; 117 } 118 119 @Override describeContents()120 public int describeContents() { 121 return 0; 122 } 123 124 @Override writeToParcel(Parcel dest, int flags)125 public void writeToParcel(Parcel dest, int flags) { 126 dest.writeInt(mSource); 127 dest.writeBoolean(mHasStatusBar); 128 dest.writeBoolean(mHasNavBar); 129 if (mBitmapBundle == null) { 130 dest.writeInt(0); 131 } else { 132 dest.writeInt(1); 133 dest.writeBundle(mBitmapBundle); 134 dest.writeParcelable(mBoundsInScreen, 0); 135 dest.writeParcelable(mInsets, 0); 136 dest.writeInt(mTaskId); 137 dest.writeInt(mUserId); 138 dest.writeParcelable(mTopComponent, 0); 139 } 140 } 141 142 public static final @NonNull Parcelable.Creator<ScreenshotRequest> CREATOR = 143 new Parcelable.Creator<ScreenshotRequest>() { 144 145 @Override 146 public ScreenshotRequest createFromParcel(Parcel source) { 147 return new ScreenshotRequest(source); 148 } 149 150 @Override 151 public ScreenshotRequest[] newArray(int size) { 152 return new ScreenshotRequest[size]; 153 } 154 }; 155 } 156 157 private static final String TAG = "ScreenshotHelper"; 158 159 // Time until we give up on the screenshot & show an error instead. 160 private final int SCREENSHOT_TIMEOUT_MS = 10000; 161 162 private final Object mScreenshotLock = new Object(); 163 private IBinder mScreenshotService = null; 164 private ServiceConnection mScreenshotConnection = null; 165 private final Context mContext; 166 167 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 168 @Override 169 public void onReceive(Context context, Intent intent) { 170 synchronized (mScreenshotLock) { 171 if (ACTION_USER_SWITCHED.equals(intent.getAction())) { 172 resetConnection(); 173 } 174 } 175 } 176 }; 177 ScreenshotHelper(Context context)178 public ScreenshotHelper(Context context) { 179 mContext = context; 180 IntentFilter filter = new IntentFilter(ACTION_USER_SWITCHED); 181 mContext.registerReceiver(mBroadcastReceiver, filter); 182 } 183 184 /** 185 * Request a screenshot be taken. 186 * 187 * Added to support reducing unit test duration; the method variant without a timeout argument 188 * is recommended for general use. 189 * 190 * @param screenshotType The type of screenshot, for example either 191 * {@link android.view.WindowManager#TAKE_SCREENSHOT_FULLSCREEN} 192 * or 193 * {@link android.view.WindowManager#TAKE_SCREENSHOT_SELECTED_REGION} 194 * @param hasStatus {@code true} if the status bar is currently showing. {@code false} 195 * if not. 196 * @param hasNav {@code true} if the navigation bar is currently showing. {@code 197 * false} if not. 198 * @param source The source of the screenshot request. One of 199 * {SCREENSHOT_GLOBAL_ACTIONS, SCREENSHOT_KEY_CHORD, 200 * SCREENSHOT_OVERVIEW, SCREENSHOT_OTHER} 201 * @param handler A handler used in case the screenshot times out 202 * @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the 203 * screenshot was taken. 204 */ takeScreenshot(final int screenshotType, final boolean hasStatus, final boolean hasNav, int source, @NonNull Handler handler, @Nullable Consumer<Uri> completionConsumer)205 public void takeScreenshot(final int screenshotType, final boolean hasStatus, 206 final boolean hasNav, int source, @NonNull Handler handler, 207 @Nullable Consumer<Uri> completionConsumer) { 208 ScreenshotRequest screenshotRequest = new ScreenshotRequest(source, hasStatus, hasNav); 209 takeScreenshot(screenshotType, SCREENSHOT_TIMEOUT_MS, handler, screenshotRequest, 210 completionConsumer); 211 } 212 213 /** 214 * Request a screenshot be taken, with provided reason. 215 * 216 * @param screenshotType The type of screenshot, for example either 217 * {@link android.view.WindowManager#TAKE_SCREENSHOT_FULLSCREEN} 218 * or 219 * {@link android.view.WindowManager#TAKE_SCREENSHOT_SELECTED_REGION} 220 * @param hasStatus {@code true} if the status bar is currently showing. {@code false} 221 * if 222 * not. 223 * @param hasNav {@code true} if the navigation bar is currently showing. {@code 224 * false} 225 * if not. 226 * @param handler A handler used in case the screenshot times out 227 * @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the 228 * screenshot was taken. 229 */ takeScreenshot(final int screenshotType, final boolean hasStatus, final boolean hasNav, @NonNull Handler handler, @Nullable Consumer<Uri> completionConsumer)230 public void takeScreenshot(final int screenshotType, final boolean hasStatus, 231 final boolean hasNav, @NonNull Handler handler, 232 @Nullable Consumer<Uri> completionConsumer) { 233 takeScreenshot(screenshotType, hasStatus, hasNav, SCREENSHOT_TIMEOUT_MS, handler, 234 completionConsumer); 235 } 236 237 /** 238 * Request a screenshot be taken with a specific timeout. 239 * 240 * Added to support reducing unit test duration; the method variant without a timeout argument 241 * is recommended for general use. 242 * 243 * @param screenshotType The type of screenshot, for example either 244 * {@link android.view.WindowManager#TAKE_SCREENSHOT_FULLSCREEN} 245 * or 246 * {@link android.view.WindowManager#TAKE_SCREENSHOT_SELECTED_REGION} 247 * @param hasStatus {@code true} if the status bar is currently showing. {@code false} 248 * if 249 * not. 250 * @param hasNav {@code true} if the navigation bar is currently showing. {@code 251 * false} 252 * if not. 253 * @param timeoutMs If the screenshot hasn't been completed within this time period, 254 * the screenshot attempt will be cancelled and `completionConsumer` 255 * will be run. 256 * @param handler A handler used in case the screenshot times out 257 * @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the 258 * screenshot was taken. 259 */ takeScreenshot(final int screenshotType, final boolean hasStatus, final boolean hasNav, long timeoutMs, @NonNull Handler handler, @Nullable Consumer<Uri> completionConsumer)260 public void takeScreenshot(final int screenshotType, final boolean hasStatus, 261 final boolean hasNav, long timeoutMs, @NonNull Handler handler, 262 @Nullable Consumer<Uri> completionConsumer) { 263 ScreenshotRequest screenshotRequest = new ScreenshotRequest(SCREENSHOT_OTHER, hasStatus, 264 hasNav); 265 takeScreenshot(screenshotType, timeoutMs, handler, screenshotRequest, completionConsumer); 266 } 267 268 /** 269 * Request that provided image be handled as if it was a screenshot. 270 * 271 * @param screenshotBundle Bundle containing the buffer and color space of the screenshot. 272 * @param boundsInScreen The bounds in screen coordinates that the bitmap orginated from. 273 * @param insets The insets that the image was shown with, inside the screenbounds. 274 * @param taskId The taskId of the task that the screen shot was taken of. 275 * @param userId The userId of user running the task provided in taskId. 276 * @param topComponent The component name of the top component running in the task. 277 * @param handler A handler used in case the screenshot times out 278 * @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the 279 * screenshot was taken. 280 */ provideScreenshot(@onNull Bundle screenshotBundle, @NonNull Rect boundsInScreen, @NonNull Insets insets, int taskId, int userId, ComponentName topComponent, int source, @NonNull Handler handler, @Nullable Consumer<Uri> completionConsumer)281 public void provideScreenshot(@NonNull Bundle screenshotBundle, @NonNull Rect boundsInScreen, 282 @NonNull Insets insets, int taskId, int userId, ComponentName topComponent, int source, 283 @NonNull Handler handler, @Nullable Consumer<Uri> completionConsumer) { 284 ScreenshotRequest screenshotRequest = 285 new ScreenshotRequest(source, screenshotBundle, boundsInScreen, insets, taskId, 286 userId, topComponent); 287 takeScreenshot(WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_TIMEOUT_MS, 288 handler, screenshotRequest, completionConsumer); 289 } 290 takeScreenshot(final int screenshotType, long timeoutMs, @NonNull Handler handler, ScreenshotRequest screenshotRequest, @Nullable Consumer<Uri> completionConsumer)291 private void takeScreenshot(final int screenshotType, long timeoutMs, @NonNull Handler handler, 292 ScreenshotRequest screenshotRequest, @Nullable Consumer<Uri> completionConsumer) { 293 synchronized (mScreenshotLock) { 294 295 final Runnable mScreenshotTimeout = () -> { 296 synchronized (mScreenshotLock) { 297 if (mScreenshotConnection != null) { 298 Log.e(TAG, "Timed out before getting screenshot capture response"); 299 resetConnection(); 300 notifyScreenshotError(); 301 } 302 } 303 if (completionConsumer != null) { 304 completionConsumer.accept(null); 305 } 306 }; 307 308 Message msg = Message.obtain(null, screenshotType, screenshotRequest); 309 310 Handler h = new Handler(handler.getLooper()) { 311 @Override 312 public void handleMessage(Message msg) { 313 switch (msg.what) { 314 case SCREENSHOT_MSG_URI: 315 if (completionConsumer != null) { 316 completionConsumer.accept((Uri) msg.obj); 317 } 318 handler.removeCallbacks(mScreenshotTimeout); 319 break; 320 case SCREENSHOT_MSG_PROCESS_COMPLETE: 321 synchronized (mScreenshotLock) { 322 resetConnection(); 323 } 324 break; 325 } 326 } 327 }; 328 msg.replyTo = new Messenger(h); 329 330 if (mScreenshotConnection == null || mScreenshotService == null) { 331 final ComponentName serviceComponent = ComponentName.unflattenFromString( 332 mContext.getResources().getString( 333 com.android.internal.R.string.config_screenshotServiceComponent)); 334 final Intent serviceIntent = new Intent(); 335 336 serviceIntent.setComponent(serviceComponent); 337 ServiceConnection conn = new ServiceConnection() { 338 @Override 339 public void onServiceConnected(ComponentName name, IBinder service) { 340 synchronized (mScreenshotLock) { 341 if (mScreenshotConnection != this) { 342 return; 343 } 344 mScreenshotService = service; 345 Messenger messenger = new Messenger(mScreenshotService); 346 347 try { 348 messenger.send(msg); 349 } catch (RemoteException e) { 350 Log.e(TAG, "Couldn't take screenshot: " + e); 351 if (completionConsumer != null) { 352 completionConsumer.accept(null); 353 } 354 } 355 } 356 } 357 358 @Override 359 public void onServiceDisconnected(ComponentName name) { 360 synchronized (mScreenshotLock) { 361 if (mScreenshotConnection != null) { 362 resetConnection(); 363 // only log an error if we're still within the timeout period 364 if (handler.hasCallbacks(mScreenshotTimeout)) { 365 Log.e(TAG, "Screenshot service disconnected"); 366 handler.removeCallbacks(mScreenshotTimeout); 367 notifyScreenshotError(); 368 } 369 } 370 } 371 } 372 }; 373 if (mContext.bindServiceAsUser(serviceIntent, conn, 374 Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, 375 UserHandle.CURRENT)) { 376 mScreenshotConnection = conn; 377 handler.postDelayed(mScreenshotTimeout, timeoutMs); 378 } 379 } else { 380 Messenger messenger = new Messenger(mScreenshotService); 381 382 try { 383 messenger.send(msg); 384 } catch (RemoteException e) { 385 Log.e(TAG, "Couldn't take screenshot: " + e); 386 if (completionConsumer != null) { 387 completionConsumer.accept(null); 388 } 389 } 390 handler.postDelayed(mScreenshotTimeout, timeoutMs); 391 } 392 } 393 } 394 395 /** 396 * Unbinds the current screenshot connection (if any). 397 */ resetConnection()398 private void resetConnection() { 399 if (mScreenshotConnection != null) { 400 mContext.unbindService(mScreenshotConnection); 401 mScreenshotConnection = null; 402 mScreenshotService = null; 403 } 404 } 405 406 /** 407 * Notifies the screenshot service to show an error. 408 */ notifyScreenshotError()409 private void notifyScreenshotError() { 410 // If the service process is killed, then ask it to clean up after itself 411 final ComponentName errorComponent = ComponentName.unflattenFromString( 412 mContext.getResources().getString( 413 com.android.internal.R.string.config_screenshotErrorReceiverComponent)); 414 // Broadcast needs to have a valid action. We'll just pick 415 // a generic one, since the receiver here doesn't care. 416 Intent errorIntent = new Intent(Intent.ACTION_USER_PRESENT); 417 errorIntent.setComponent(errorComponent); 418 errorIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT | 419 Intent.FLAG_RECEIVER_FOREGROUND); 420 mContext.sendBroadcastAsUser(errorIntent, UserHandle.CURRENT); 421 } 422 423 } 424