blob: 49c9302eeb110450b1ef3f6eaffc11e374ac3572 [file] [log] [blame]
Alison Cichowlasa2cd19e2017-12-06 10:51:21 -05001package com.android.internal.util;
2
Miranda Kephart7b2c3132020-03-27 09:54:14 -04003import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER;
4
Phil Weaverd0429742018-01-16 15:32:30 -08005import android.annotation.NonNull;
James O'Learyfa5bb7a2019-09-05 13:43:29 -04006import android.annotation.Nullable;
Alison Cichowlasa2cd19e2017-12-06 10:51:21 -05007import android.content.ComponentName;
8import android.content.Context;
9import android.content.Intent;
10import android.content.ServiceConnection;
Zak Cohenebd3b8b2020-01-17 11:16:00 -080011import android.graphics.Bitmap;
12import android.graphics.Insets;
13import android.graphics.Rect;
Aran Inkeb565fc2019-11-15 15:52:31 -050014import android.net.Uri;
Alison Cichowlasa2cd19e2017-12-06 10:51:21 -050015import android.os.Handler;
16import android.os.IBinder;
17import android.os.Message;
18import android.os.Messenger;
Miranda Kephart7b2c3132020-03-27 09:54:14 -040019import android.os.Parcel;
20import android.os.Parcelable;
Alison Cichowlasa2cd19e2017-12-06 10:51:21 -050021import android.os.RemoteException;
22import android.os.UserHandle;
23import android.util.Log;
Zak Cohenebd3b8b2020-01-17 11:16:00 -080024import android.view.WindowManager;
Alison Cichowlasa2cd19e2017-12-06 10:51:21 -050025
James O'Learyfa5bb7a2019-09-05 13:43:29 -040026import java.util.function.Consumer;
27
Alison Cichowlasa2cd19e2017-12-06 10:51:21 -050028public class ScreenshotHelper {
Miranda Kephart7b2c3132020-03-27 09:54:14 -040029
Miranda Kepharta4c79752020-04-24 15:23:58 -040030 public static final int SCREENSHOT_MSG_URI = 1;
31 public static final int SCREENSHOT_MSG_PROCESS_COMPLETE = 2;
32
Miranda Kephart7b2c3132020-03-27 09:54:14 -040033 /**
34 * Describes a screenshot request (to make it easier to pass data through to the handler).
35 */
36 public static class ScreenshotRequest implements Parcelable {
37 private int mSource;
38 private boolean mHasStatusBar;
39 private boolean mHasNavBar;
40 private Bitmap mBitmap;
41 private Rect mBoundsInScreen;
42 private Insets mInsets;
43 private int mTaskId;
44
45 ScreenshotRequest(int source, boolean hasStatus, boolean hasNav) {
46 mSource = source;
47 mHasStatusBar = hasStatus;
48 mHasNavBar = hasNav;
49 }
50
51 ScreenshotRequest(
52 int source, Bitmap bitmap, Rect boundsInScreen, Insets insets, int taskId) {
53 mSource = source;
54 mBitmap = bitmap;
55 mBoundsInScreen = boundsInScreen;
56 mInsets = insets;
57 mTaskId = taskId;
58 }
59
60 ScreenshotRequest(Parcel in) {
61 mSource = in.readInt();
62 mHasStatusBar = in.readBoolean();
63 mHasNavBar = in.readBoolean();
64 if (in.readInt() == 1) {
65 mBitmap = in.readParcelable(Bitmap.class.getClassLoader());
66 mBoundsInScreen = in.readParcelable(Rect.class.getClassLoader());
67 mInsets = in.readParcelable(Insets.class.getClassLoader());
68 mTaskId = in.readInt();
69 }
70 }
71
72 public int getSource() {
73 return mSource;
74 }
75
76 public boolean getHasStatusBar() {
77 return mHasStatusBar;
78 }
79
80 public boolean getHasNavBar() {
81 return mHasNavBar;
82 }
83
84 public Bitmap getBitmap() {
85 return mBitmap;
86 }
87
88 public Rect getBoundsInScreen() {
89 return mBoundsInScreen;
90 }
91
92 public Insets getInsets() {
93 return mInsets;
94 }
95
96 public int getTaskId() {
97 return mTaskId;
98 }
99
100 @Override
101 public int describeContents() {
102 return 0;
103 }
104
105 @Override
106 public void writeToParcel(Parcel dest, int flags) {
107 dest.writeInt(mSource);
108 dest.writeBoolean(mHasStatusBar);
109 dest.writeBoolean(mHasNavBar);
110 if (mBitmap == null) {
111 dest.writeInt(0);
112 } else {
113 dest.writeInt(1);
114 dest.writeParcelable(mBitmap, 0);
115 dest.writeParcelable(mBoundsInScreen, 0);
116 dest.writeParcelable(mInsets, 0);
117 dest.writeInt(mTaskId);
118 }
119 }
120
121 public static final @NonNull Parcelable.Creator<ScreenshotRequest> CREATOR =
122 new Parcelable.Creator<ScreenshotRequest>() {
123
124 @Override
125 public ScreenshotRequest createFromParcel(Parcel source) {
126 return new ScreenshotRequest(source);
127 }
128
129 @Override
130 public ScreenshotRequest[] newArray(int size) {
131 return new ScreenshotRequest[size];
132 }
133 };
134 }
Alison Cichowlasa2cd19e2017-12-06 10:51:21 -0500135 private static final String TAG = "ScreenshotHelper";
136
Alison Cichowlasa2cd19e2017-12-06 10:51:21 -0500137 // Time until we give up on the screenshot & show an error instead.
138 private final int SCREENSHOT_TIMEOUT_MS = 10000;
139
140 private final Object mScreenshotLock = new Object();
Miranda Kepharta4c79752020-04-24 15:23:58 -0400141 private IBinder mScreenshotService = null;
Alison Cichowlasa2cd19e2017-12-06 10:51:21 -0500142 private ServiceConnection mScreenshotConnection = null;
143 private final Context mContext;
144
145 public ScreenshotHelper(Context context) {
146 mContext = context;
147 }
148
Miranda Kephart7b2c3132020-03-27 09:54:14 -0400149
150
Phil Weaverd0429742018-01-16 15:32:30 -0800151 /**
Miranda Kephart7b2c3132020-03-27 09:54:14 -0400152 * Request a screenshot be taken.
Phil Weaverd0429742018-01-16 15:32:30 -0800153 *
James O'Learyfa5bb7a2019-09-05 13:43:29 -0400154 * Added to support reducing unit test duration; the method variant without a timeout argument
155 * is recommended for general use.
156 *
157 * @param screenshotType The type of screenshot, for example either
Zak Cohenebd3b8b2020-01-17 11:16:00 -0800158 * {@link android.view.WindowManager#TAKE_SCREENSHOT_FULLSCREEN}
James O'Learyfa5bb7a2019-09-05 13:43:29 -0400159 * or
Zak Cohenebd3b8b2020-01-17 11:16:00 -0800160 * {@link android.view.WindowManager#TAKE_SCREENSHOT_SELECTED_REGION}
James O'Learyfa5bb7a2019-09-05 13:43:29 -0400161 * @param hasStatus {@code true} if the status bar is currently showing. {@code false}
Miranda Kephart7b2c3132020-03-27 09:54:14 -0400162 * if not.
163 * @param hasNav {@code true} if the navigation bar is currently showing. {@code
164 * false} if not.
165 * @param source The source of the screenshot request. One of
166 * {SCREENSHOT_GLOBAL_ACTIONS, SCREENSHOT_KEY_CHORD,
167 * SCREENSHOT_OVERVIEW, SCREENSHOT_OTHER}
168 * @param handler A handler used in case the screenshot times out
169 * @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the
170 * screenshot was taken.
171 */
172 public void takeScreenshot(final int screenshotType, final boolean hasStatus,
173 final boolean hasNav, int source, @NonNull Handler handler,
174 @Nullable Consumer<Uri> completionConsumer) {
175 ScreenshotRequest screenshotRequest = new ScreenshotRequest(source, hasStatus, hasNav);
176 takeScreenshot(screenshotType, SCREENSHOT_TIMEOUT_MS, handler, screenshotRequest,
177 completionConsumer);
178 }
179
180 /**
181 * Request a screenshot be taken, with provided reason.
182 *
183 * @param screenshotType The type of screenshot, for example either
184 * {@link android.view.WindowManager#TAKE_SCREENSHOT_FULLSCREEN}
185 * or
186 * {@link android.view.WindowManager#TAKE_SCREENSHOT_SELECTED_REGION}
187 * @param hasStatus {@code true} if the status bar is currently showing. {@code false}
James O'Learyfa5bb7a2019-09-05 13:43:29 -0400188 * if
189 * not.
190 * @param hasNav {@code true} if the navigation bar is currently showing. {@code
191 * false}
192 * if not.
193 * @param handler A handler used in case the screenshot times out
194 * @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the
195 * screenshot was taken.
Phil Weaverd0429742018-01-16 15:32:30 -0800196 */
Alison Cichowlasa2cd19e2017-12-06 10:51:21 -0500197 public void takeScreenshot(final int screenshotType, final boolean hasStatus,
James O'Learyfa5bb7a2019-09-05 13:43:29 -0400198 final boolean hasNav, @NonNull Handler handler,
Aran Inkeb565fc2019-11-15 15:52:31 -0500199 @Nullable Consumer<Uri> completionConsumer) {
James O'Learyfa5bb7a2019-09-05 13:43:29 -0400200 takeScreenshot(screenshotType, hasStatus, hasNav, SCREENSHOT_TIMEOUT_MS, handler,
201 completionConsumer);
202 }
203
204 /**
Miranda Kephart7b2c3132020-03-27 09:54:14 -0400205 * Request a screenshot be taken with a specific timeout.
James O'Learyfa5bb7a2019-09-05 13:43:29 -0400206 *
207 * Added to support reducing unit test duration; the method variant without a timeout argument
208 * is recommended for general use.
209 *
210 * @param screenshotType The type of screenshot, for example either
Zak Cohenebd3b8b2020-01-17 11:16:00 -0800211 * {@link android.view.WindowManager#TAKE_SCREENSHOT_FULLSCREEN}
James O'Learyfa5bb7a2019-09-05 13:43:29 -0400212 * or
Zak Cohenebd3b8b2020-01-17 11:16:00 -0800213 * {@link android.view.WindowManager#TAKE_SCREENSHOT_SELECTED_REGION}
James O'Learyfa5bb7a2019-09-05 13:43:29 -0400214 * @param hasStatus {@code true} if the status bar is currently showing. {@code false}
215 * if
216 * not.
217 * @param hasNav {@code true} if the navigation bar is currently showing. {@code
218 * false}
219 * if not.
220 * @param timeoutMs If the screenshot hasn't been completed within this time period,
221 * the screenshot attempt will be cancelled and `completionConsumer`
222 * will be run.
223 * @param handler A handler used in case the screenshot times out
224 * @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the
225 * screenshot was taken.
226 */
227 public void takeScreenshot(final int screenshotType, final boolean hasStatus,
228 final boolean hasNav, long timeoutMs, @NonNull Handler handler,
Aran Inkeb565fc2019-11-15 15:52:31 -0500229 @Nullable Consumer<Uri> completionConsumer) {
Miranda Kephart7b2c3132020-03-27 09:54:14 -0400230 ScreenshotRequest screenshotRequest = new ScreenshotRequest(SCREENSHOT_OTHER, hasStatus,
231 hasNav);
232 takeScreenshot(screenshotType, timeoutMs, handler, screenshotRequest, completionConsumer);
Zak Cohenebd3b8b2020-01-17 11:16:00 -0800233 }
234
235 /**
236 * Request that provided image be handled as if it was a screenshot.
237 *
238 * @param screenshot The bitmap to treat as the screen shot.
239 * @param boundsInScreen The bounds in screen coordinates that the bitmap orginated from.
240 * @param insets The insets that the image was shown with, inside the screenbounds.
241 * @param taskId The taskId of the task that the screen shot was taken of.
242 * @param handler A handler used in case the screenshot times out
243 * @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the
244 * screenshot was taken.
245 */
246 public void provideScreenshot(@NonNull Bitmap screenshot, @NonNull Rect boundsInScreen,
Miranda Kephart7b2c3132020-03-27 09:54:14 -0400247 @NonNull Insets insets, int taskId, int source,
248 @NonNull Handler handler, @Nullable Consumer<Uri> completionConsumer) {
249 ScreenshotRequest screenshotRequest =
250 new ScreenshotRequest(source, screenshot, boundsInScreen, insets, taskId);
251 takeScreenshot(WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_TIMEOUT_MS,
252 handler, screenshotRequest, completionConsumer);
Zak Cohenebd3b8b2020-01-17 11:16:00 -0800253 }
254
Miranda Kephart7b2c3132020-03-27 09:54:14 -0400255 private void takeScreenshot(final int screenshotType, long timeoutMs, @NonNull Handler handler,
256 ScreenshotRequest screenshotRequest, @Nullable Consumer<Uri> completionConsumer) {
Alison Cichowlasa2cd19e2017-12-06 10:51:21 -0500257 synchronized (mScreenshotLock) {
Alison Cichowlasa2cd19e2017-12-06 10:51:21 -0500258
Miranda Kepharta4c79752020-04-24 15:23:58 -0400259 final Runnable mScreenshotTimeout = () -> {
260 synchronized (mScreenshotLock) {
261 if (mScreenshotConnection != null) {
262 mContext.unbindService(mScreenshotConnection);
263 mScreenshotConnection = null;
264 mScreenshotService = null;
265 notifyScreenshotError();
266 }
267 }
268 if (completionConsumer != null) {
269 completionConsumer.accept(null);
270 }
271 };
272
273 Message msg = Message.obtain(null, screenshotType, screenshotRequest);
274 final ServiceConnection myConn = mScreenshotConnection;
275 Handler h = new Handler(handler.getLooper()) {
James O'Learyfa5bb7a2019-09-05 13:43:29 -0400276 @Override
Miranda Kepharta4c79752020-04-24 15:23:58 -0400277 public void handleMessage(Message msg) {
278 switch (msg.what) {
279 case SCREENSHOT_MSG_URI:
280 if (completionConsumer != null) {
281 completionConsumer.accept((Uri) msg.obj);
282 }
283 handler.removeCallbacks(mScreenshotTimeout);
284 break;
285 case SCREENSHOT_MSG_PROCESS_COMPLETE:
286 synchronized (mScreenshotLock) {
287 if (mScreenshotConnection == myConn) {
288 mContext.unbindService(mScreenshotConnection);
289 mScreenshotConnection = null;
290 mScreenshotService = null;
291 }
292 }
293 break;
294 }
295 }
296 };
297 msg.replyTo = new Messenger(h);
298
299 if (mScreenshotConnection == null) {
300 final ComponentName serviceComponent = ComponentName.unflattenFromString(
301 mContext.getResources().getString(
302 com.android.internal.R.string.config_screenshotServiceComponent));
303 final Intent serviceIntent = new Intent();
304
305 serviceIntent.setComponent(serviceComponent);
306 ServiceConnection conn = new ServiceConnection() {
307 @Override
308 public void onServiceConnected(ComponentName name, IBinder service) {
309 synchronized (mScreenshotLock) {
310 if (mScreenshotConnection != this) {
311 return;
312 }
313 mScreenshotService = service;
314 Messenger messenger = new Messenger(mScreenshotService);
315
316 try {
317 messenger.send(msg);
318 } catch (RemoteException e) {
319 Log.e(TAG, "Couldn't take screenshot: " + e);
320 if (completionConsumer != null) {
321 completionConsumer.accept(null);
322 }
323 }
Alison Cichowlasa2cd19e2017-12-06 10:51:21 -0500324 }
325 }
Miranda Kepharta4c79752020-04-24 15:23:58 -0400326
327 @Override
328 public void onServiceDisconnected(ComponentName name) {
329 synchronized (mScreenshotLock) {
330 if (mScreenshotConnection != null) {
331 mContext.unbindService(mScreenshotConnection);
332 mScreenshotConnection = null;
333 mScreenshotService = null;
334 handler.removeCallbacks(mScreenshotTimeout);
335 notifyScreenshotError();
336 }
337 }
338 }
339 };
340 if (mContext.bindServiceAsUser(serviceIntent, conn,
341 Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
342 UserHandle.CURRENT)) {
343 mScreenshotConnection = conn;
344 handler.postDelayed(mScreenshotTimeout, timeoutMs);
345 }
346 } else {
347 Messenger messenger = new Messenger(mScreenshotService);
348 try {
349 messenger.send(msg);
350 } catch (RemoteException e) {
351 Log.e(TAG, "Couldn't take screenshot: " + e);
James O'Learyfa5bb7a2019-09-05 13:43:29 -0400352 if (completionConsumer != null) {
Aran Inkeb565fc2019-11-15 15:52:31 -0500353 completionConsumer.accept(null);
James O'Learyfa5bb7a2019-09-05 13:43:29 -0400354 }
Alison Cichowlasa2cd19e2017-12-06 10:51:21 -0500355 }
James O'Learyfa5bb7a2019-09-05 13:43:29 -0400356 handler.postDelayed(mScreenshotTimeout, timeoutMs);
Alison Cichowlasa2cd19e2017-12-06 10:51:21 -0500357 }
358 }
359 }
360
361 /**
362 * Notifies the screenshot service to show an error.
363 */
364 private void notifyScreenshotError() {
365 // If the service process is killed, then ask it to clean up after itself
Winson Chungf2b41772019-11-06 15:00:48 -0800366 final ComponentName errorComponent = ComponentName.unflattenFromString(
367 mContext.getResources().getString(
368 com.android.internal.R.string.config_screenshotErrorReceiverComponent));
Alison Cichowlasa2cd19e2017-12-06 10:51:21 -0500369 // Broadcast needs to have a valid action. We'll just pick
370 // a generic one, since the receiver here doesn't care.
371 Intent errorIntent = new Intent(Intent.ACTION_USER_PRESENT);
372 errorIntent.setComponent(errorComponent);
373 errorIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT |
374 Intent.FLAG_RECEIVER_FOREGROUND);
375 mContext.sendBroadcastAsUser(errorIntent, UserHandle.CURRENT);
376 }
377
378}