• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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