• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 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 com.android.systemui.screenshot;
18 
19 import static android.content.Context.NOTIFICATION_SERVICE;
20 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
21 
22 import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ACTION_INTENT;
23 import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_CANCEL_NOTIFICATION;
24 import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_DISALLOW_ENTER_PIP;
25 import static com.android.systemui.statusbar.phone.StatusBar.SYSTEM_DIALOG_REASON_SCREENSHOT;
26 
27 import android.animation.Animator;
28 import android.animation.AnimatorListenerAdapter;
29 import android.animation.AnimatorSet;
30 import android.animation.ValueAnimator;
31 import android.animation.ValueAnimator.AnimatorUpdateListener;
32 import android.app.ActivityOptions;
33 import android.app.Notification;
34 import android.app.Notification.BigPictureStyle;
35 import android.app.NotificationManager;
36 import android.app.PendingIntent;
37 import android.app.admin.DevicePolicyManager;
38 import android.content.BroadcastReceiver;
39 import android.content.ClipData;
40 import android.content.ClipDescription;
41 import android.content.ComponentName;
42 import android.content.ContentResolver;
43 import android.content.Context;
44 import android.content.Intent;
45 import android.content.res.Configuration;
46 import android.content.res.Resources;
47 import android.graphics.Bitmap;
48 import android.graphics.Canvas;
49 import android.graphics.Color;
50 import android.graphics.ColorMatrix;
51 import android.graphics.ColorMatrixColorFilter;
52 import android.graphics.Matrix;
53 import android.graphics.Paint;
54 import android.graphics.Picture;
55 import android.graphics.PixelFormat;
56 import android.graphics.PointF;
57 import android.graphics.Rect;
58 import android.media.MediaActionSound;
59 import android.net.Uri;
60 import android.os.AsyncTask;
61 import android.os.Environment;
62 import android.os.PowerManager;
63 import android.os.Process;
64 import android.os.UserHandle;
65 import android.provider.MediaStore;
66 import android.text.TextUtils;
67 import android.util.DisplayMetrics;
68 import android.util.Slog;
69 import android.view.Display;
70 import android.view.LayoutInflater;
71 import android.view.MotionEvent;
72 import android.view.SurfaceControl;
73 import android.view.View;
74 import android.view.ViewGroup;
75 import android.view.WindowManager;
76 import android.view.animation.Interpolator;
77 import android.widget.ImageView;
78 import android.widget.Toast;
79 
80 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
81 import com.android.systemui.R;
82 import com.android.systemui.SysUiServiceProvider;
83 import com.android.systemui.SystemUI;
84 import com.android.systemui.shared.system.ActivityManagerWrapper;
85 import com.android.systemui.statusbar.phone.StatusBar;
86 import com.android.systemui.util.NotificationChannels;
87 
88 import libcore.io.IoUtils;
89 
90 import java.io.IOException;
91 import java.io.OutputStream;
92 import java.text.DateFormat;
93 import java.text.SimpleDateFormat;
94 import java.util.Date;
95 import java.util.concurrent.ExecutionException;
96 import java.util.concurrent.TimeUnit;
97 import java.util.concurrent.TimeoutException;
98 
99 
100 
101 /**
102  * POD used in the AsyncTask which saves an image in the background.
103  */
104 class SaveImageInBackgroundData {
105     Context context;
106     Bitmap image;
107     Uri imageUri;
108     Runnable finisher;
109     int iconSize;
110     int previewWidth;
111     int previewheight;
112     int errorMsgResId;
113 
clearImage()114     void clearImage() {
115         image = null;
116         imageUri = null;
117         iconSize = 0;
118     }
clearContext()119     void clearContext() {
120         context = null;
121     }
122 }
123 
124 /**
125  * An AsyncTask that saves an image to the media store in the background.
126  */
127 class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
128     private static final String TAG = "SaveImageInBackgroundTask";
129 
130     private static final String SCREENSHOT_FILE_NAME_TEMPLATE = "Screenshot_%s.png";
131     private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)";
132 
133     private final SaveImageInBackgroundData mParams;
134     private final NotificationManager mNotificationManager;
135     private final Notification.Builder mNotificationBuilder, mPublicNotificationBuilder;
136     private final String mImageFileName;
137     private final long mImageTime;
138     private final BigPictureStyle mNotificationStyle;
139     private final int mImageWidth;
140     private final int mImageHeight;
141 
SaveImageInBackgroundTask(Context context, SaveImageInBackgroundData data, NotificationManager nManager)142     SaveImageInBackgroundTask(Context context, SaveImageInBackgroundData data,
143             NotificationManager nManager) {
144         Resources r = context.getResources();
145 
146         // Prepare all the output metadata
147         mParams = data;
148         mImageTime = System.currentTimeMillis();
149         String imageDate = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(mImageTime));
150         mImageFileName = String.format(SCREENSHOT_FILE_NAME_TEMPLATE, imageDate);
151 
152         // Create the large notification icon
153         mImageWidth = data.image.getWidth();
154         mImageHeight = data.image.getHeight();
155         int iconSize = data.iconSize;
156         int previewWidth = data.previewWidth;
157         int previewHeight = data.previewheight;
158 
159         Paint paint = new Paint();
160         ColorMatrix desat = new ColorMatrix();
161         desat.setSaturation(0.25f);
162         paint.setColorFilter(new ColorMatrixColorFilter(desat));
163         Matrix matrix = new Matrix();
164         int overlayColor = 0x40FFFFFF;
165 
166         matrix.setTranslate((previewWidth - mImageWidth) / 2, (previewHeight - mImageHeight) / 2);
167         Bitmap picture = generateAdjustedHwBitmap(data.image, previewWidth, previewHeight, matrix,
168                 paint, overlayColor);
169 
170         // Note, we can't use the preview for the small icon, since it is non-square
171         float scale = (float) iconSize / Math.min(mImageWidth, mImageHeight);
172         matrix.setScale(scale, scale);
173         matrix.postTranslate((iconSize - (scale * mImageWidth)) / 2,
174                 (iconSize - (scale * mImageHeight)) / 2);
175         Bitmap icon = generateAdjustedHwBitmap(data.image, iconSize, iconSize, matrix, paint,
176                 overlayColor);
177 
178         mNotificationManager = nManager;
179         final long now = System.currentTimeMillis();
180 
181         // Setup the notification
182         mNotificationStyle = new Notification.BigPictureStyle()
183                 .bigPicture(picture.createAshmemBitmap());
184 
185         // The public notification will show similar info but with the actual screenshot omitted
186         mPublicNotificationBuilder =
187                 new Notification.Builder(context, NotificationChannels.SCREENSHOTS_HEADSUP)
188                         .setContentTitle(r.getString(R.string.screenshot_saving_title))
189                         .setSmallIcon(R.drawable.stat_notify_image)
190                         .setCategory(Notification.CATEGORY_PROGRESS)
191                         .setWhen(now)
192                         .setShowWhen(true)
193                         .setColor(r.getColor(
194                                 com.android.internal.R.color.system_notification_accent_color));
195         SystemUI.overrideNotificationAppName(context, mPublicNotificationBuilder, true);
196 
197         mNotificationBuilder = new Notification.Builder(context,
198                 NotificationChannels.SCREENSHOTS_HEADSUP)
199             .setContentTitle(r.getString(R.string.screenshot_saving_title))
200             .setSmallIcon(R.drawable.stat_notify_image)
201             .setWhen(now)
202             .setShowWhen(true)
203             .setColor(r.getColor(com.android.internal.R.color.system_notification_accent_color))
204             .setStyle(mNotificationStyle)
205             .setPublicVersion(mPublicNotificationBuilder.build());
206         mNotificationBuilder.setFlag(Notification.FLAG_NO_CLEAR, true);
207         SystemUI.overrideNotificationAppName(context, mNotificationBuilder, true);
208 
209         mNotificationManager.notify(SystemMessage.NOTE_GLOBAL_SCREENSHOT,
210                 mNotificationBuilder.build());
211 
212         /**
213          * NOTE: The following code prepares the notification builder for updating the notification
214          * after the screenshot has been written to disk.
215          */
216 
217         // On the tablet, the large icon makes the notification appear as if it is clickable (and
218         // on small devices, the large icon is not shown) so defer showing the large icon until
219         // we compose the final post-save notification below.
220         mNotificationBuilder.setLargeIcon(icon.createAshmemBitmap());
221         // But we still don't set it for the expanded view, allowing the smallIcon to show here.
222         mNotificationStyle.bigLargeIcon((Bitmap) null);
223     }
224 
225     /**
226      * Generates a new hardware bitmap with specified values, copying the content from the passed
227      * in bitmap.
228      */
generateAdjustedHwBitmap(Bitmap bitmap, int width, int height, Matrix matrix, Paint paint, int color)229     private Bitmap generateAdjustedHwBitmap(Bitmap bitmap, int width, int height, Matrix matrix,
230             Paint paint, int color) {
231         Picture picture = new Picture();
232         Canvas canvas = picture.beginRecording(width, height);
233         canvas.drawColor(color);
234         canvas.drawBitmap(bitmap, matrix, paint);
235         picture.endRecording();
236         return Bitmap.createBitmap(picture);
237     }
238 
239     @Override
doInBackground(Void... paramsUnused)240     protected Void doInBackground(Void... paramsUnused) {
241         if (isCancelled()) {
242             return null;
243         }
244 
245         // By default, AsyncTask sets the worker thread to have background thread priority, so bump
246         // it back up so that we save a little quicker.
247         Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND);
248 
249         Context context = mParams.context;
250         Bitmap image = mParams.image;
251         Resources r = context.getResources();
252 
253         try {
254             // Save the screenshot to the MediaStore
255             final MediaStore.PendingParams params = new MediaStore.PendingParams(
256                     MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mImageFileName, "image/png");
257             params.setPrimaryDirectory(Environment.DIRECTORY_PICTURES);
258             params.setSecondaryDirectory(Environment.DIRECTORY_SCREENSHOTS);
259 
260             final Uri uri = MediaStore.createPending(context, params);
261             final MediaStore.PendingSession session = MediaStore.openPending(context, uri);
262             try {
263                 try (OutputStream out = session.openOutputStream()) {
264                     if (!image.compress(Bitmap.CompressFormat.PNG, 100, out)) {
265                         throw new IOException("Failed to compress");
266                     }
267                 }
268                 session.publish();
269             } catch (Exception e) {
270                 session.abandon();
271                 throw e;
272             } finally {
273                 IoUtils.closeQuietly(session);
274             }
275 
276             // Note: Both the share and edit actions are proxied through ActionProxyReceiver in
277             // order to do some common work like dismissing the keyguard and sending
278             // closeSystemWindows
279 
280             // Create a share intent, this will always go through the chooser activity first which
281             // should not trigger auto-enter PiP
282             String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime));
283             String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
284             Intent sharingIntent = new Intent(Intent.ACTION_SEND);
285             sharingIntent.setType("image/png");
286             sharingIntent.putExtra(Intent.EXTRA_STREAM, uri);
287             // Include URI in ClipData also, so that grantPermission picks it up.
288             // We don't use setData here because some apps interpret this as "to:".
289             ClipData clipdata = new ClipData(new ClipDescription("content",
290                     new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}),
291                     new ClipData.Item(uri));
292             sharingIntent.setClipData(clipdata);
293             sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
294             sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
295 
296             PendingIntent chooserAction = PendingIntent.getBroadcast(context, 0,
297                     new Intent(context, GlobalScreenshot.TargetChosenReceiver.class),
298                     PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
299             Intent sharingChooserIntent = Intent.createChooser(sharingIntent, null,
300                     chooserAction.getIntentSender())
301                     .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK)
302                     .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
303 
304             // Create a share action for the notification
305             PendingIntent shareAction = PendingIntent.getBroadcastAsUser(context, 0,
306                     new Intent(context, GlobalScreenshot.ActionProxyReceiver.class)
307                             .putExtra(EXTRA_ACTION_INTENT, sharingChooserIntent)
308                             .putExtra(EXTRA_DISALLOW_ENTER_PIP, true),
309                     PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.SYSTEM);
310             Notification.Action.Builder shareActionBuilder = new Notification.Action.Builder(
311                     R.drawable.ic_screenshot_share,
312                     r.getString(com.android.internal.R.string.share), shareAction);
313             mNotificationBuilder.addAction(shareActionBuilder.build());
314 
315             // Create an edit intent, if a specific package is provided as the editor, then launch
316             // that directly
317             String editorPackage = context.getString(R.string.config_screenshotEditor);
318             Intent editIntent = new Intent(Intent.ACTION_EDIT);
319             if (!TextUtils.isEmpty(editorPackage)) {
320                 editIntent.setComponent(ComponentName.unflattenFromString(editorPackage));
321             }
322             editIntent.setType("image/png");
323             editIntent.setData(uri);
324             editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
325             editIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
326 
327             // Create a edit action
328             PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, 1,
329                     new Intent(context, GlobalScreenshot.ActionProxyReceiver.class)
330                             .putExtra(EXTRA_ACTION_INTENT, editIntent)
331                             .putExtra(EXTRA_CANCEL_NOTIFICATION, editIntent.getComponent() != null),
332                     PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.SYSTEM);
333             Notification.Action.Builder editActionBuilder = new Notification.Action.Builder(
334                     R.drawable.ic_screenshot_edit,
335                     r.getString(com.android.internal.R.string.screenshot_edit), editAction);
336             mNotificationBuilder.addAction(editActionBuilder.build());
337 
338             // Create a delete action for the notification
339             PendingIntent deleteAction = PendingIntent.getBroadcast(context, 0,
340                     new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class)
341                             .putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString()),
342                     PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
343             Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder(
344                     R.drawable.ic_screenshot_delete,
345                     r.getString(com.android.internal.R.string.delete), deleteAction);
346             mNotificationBuilder.addAction(deleteActionBuilder.build());
347 
348             mParams.imageUri = uri;
349             mParams.image = null;
350             mParams.errorMsgResId = 0;
351         } catch (Exception e) {
352             // IOException/UnsupportedOperationException may be thrown if external storage is not
353             // mounted
354             Slog.e(TAG, "unable to save screenshot", e);
355             mParams.clearImage();
356             mParams.errorMsgResId = R.string.screenshot_failed_to_save_text;
357         }
358 
359         // Recycle the bitmap data
360         if (image != null) {
361             image.recycle();
362         }
363 
364         return null;
365     }
366 
367     @Override
onPostExecute(Void params)368     protected void onPostExecute(Void params) {
369         if (mParams.errorMsgResId != 0) {
370             // Show a message that we've failed to save the image to disk
371             GlobalScreenshot.notifyScreenshotError(mParams.context, mNotificationManager,
372                     mParams.errorMsgResId);
373         } else {
374             // Show the final notification to indicate screenshot saved
375             Context context = mParams.context;
376             Resources r = context.getResources();
377 
378             // Create the intent to show the screenshot in gallery
379             Intent launchIntent = new Intent(Intent.ACTION_VIEW);
380             launchIntent.setDataAndType(mParams.imageUri, "image/png");
381             launchIntent.setFlags(
382                     Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
383 
384             final long now = System.currentTimeMillis();
385 
386             // Update the text and the icon for the existing notification
387             mPublicNotificationBuilder
388                     .setContentTitle(r.getString(R.string.screenshot_saved_title))
389                     .setContentText(r.getString(R.string.screenshot_saved_text))
390                     .setContentIntent(PendingIntent.getActivity(mParams.context, 0, launchIntent, 0))
391                     .setWhen(now)
392                     .setAutoCancel(true)
393                     .setColor(context.getColor(
394                             com.android.internal.R.color.system_notification_accent_color));
395             mNotificationBuilder
396                 .setContentTitle(r.getString(R.string.screenshot_saved_title))
397                 .setContentText(r.getString(R.string.screenshot_saved_text))
398                 .setContentIntent(PendingIntent.getActivity(mParams.context, 0, launchIntent, 0))
399                 .setWhen(now)
400                 .setAutoCancel(true)
401                 .setColor(context.getColor(
402                         com.android.internal.R.color.system_notification_accent_color))
403                 .setPublicVersion(mPublicNotificationBuilder.build())
404                 .setFlag(Notification.FLAG_NO_CLEAR, false);
405 
406             mNotificationManager.notify(SystemMessage.NOTE_GLOBAL_SCREENSHOT,
407                     mNotificationBuilder.build());
408         }
409         mParams.finisher.run();
410         mParams.clearContext();
411     }
412 
413     @Override
onCancelled(Void params)414     protected void onCancelled(Void params) {
415         // If we are cancelled while the task is running in the background, we may get null params.
416         // The finisher is expected to always be called back, so just use the baked-in params from
417         // the ctor in any case.
418         mParams.finisher.run();
419         mParams.clearImage();
420         mParams.clearContext();
421 
422         // Cancel the posted notification
423         mNotificationManager.cancel(SystemMessage.NOTE_GLOBAL_SCREENSHOT);
424     }
425 }
426 
427 /**
428  * An AsyncTask that deletes an image from the media store in the background.
429  */
430 class DeleteImageInBackgroundTask extends AsyncTask<Uri, Void, Void> {
431     private Context mContext;
432 
DeleteImageInBackgroundTask(Context context)433     DeleteImageInBackgroundTask(Context context) {
434         mContext = context;
435     }
436 
437     @Override
doInBackground(Uri... params)438     protected Void doInBackground(Uri... params) {
439         if (params.length != 1) return null;
440 
441         Uri screenshotUri = params[0];
442         ContentResolver resolver = mContext.getContentResolver();
443         resolver.delete(screenshotUri, null, null);
444         return null;
445     }
446 }
447 
448 class GlobalScreenshot {
449     static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id";
450     static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent";
451     static final String EXTRA_CANCEL_NOTIFICATION = "android:screenshot_cancel_notification";
452     static final String EXTRA_DISALLOW_ENTER_PIP = "android:screenshot_disallow_enter_pip";
453 
454     private static final String TAG = "GlobalScreenshot";
455 
456     private static final int SCREENSHOT_FLASH_TO_PEAK_DURATION = 130;
457     private static final int SCREENSHOT_DROP_IN_DURATION = 430;
458     private static final int SCREENSHOT_DROP_OUT_DELAY = 500;
459     private static final int SCREENSHOT_DROP_OUT_DURATION = 430;
460     private static final int SCREENSHOT_DROP_OUT_SCALE_DURATION = 370;
461     private static final int SCREENSHOT_FAST_DROP_OUT_DURATION = 320;
462     private static final float BACKGROUND_ALPHA = 0.5f;
463     private static final float SCREENSHOT_SCALE = 1f;
464     private static final float SCREENSHOT_DROP_IN_MIN_SCALE = SCREENSHOT_SCALE * 0.725f;
465     private static final float SCREENSHOT_DROP_OUT_MIN_SCALE = SCREENSHOT_SCALE * 0.45f;
466     private static final float SCREENSHOT_FAST_DROP_OUT_MIN_SCALE = SCREENSHOT_SCALE * 0.6f;
467     private static final float SCREENSHOT_DROP_OUT_MIN_SCALE_OFFSET = 0f;
468     private final int mPreviewWidth;
469     private final int mPreviewHeight;
470 
471     private Context mContext;
472     private WindowManager mWindowManager;
473     private WindowManager.LayoutParams mWindowLayoutParams;
474     private NotificationManager mNotificationManager;
475     private Display mDisplay;
476     private DisplayMetrics mDisplayMetrics;
477 
478     private Bitmap mScreenBitmap;
479     private View mScreenshotLayout;
480     private ScreenshotSelectorView mScreenshotSelectorView;
481     private ImageView mBackgroundView;
482     private ImageView mScreenshotView;
483     private ImageView mScreenshotFlash;
484 
485     private AnimatorSet mScreenshotAnimation;
486 
487     private int mNotificationIconSize;
488     private float mBgPadding;
489     private float mBgPaddingScale;
490 
491     private AsyncTask<Void, Void, Void> mSaveInBgTask;
492 
493     private MediaActionSound mCameraSound;
494 
495 
496     /**
497      * @param context everything needs a context :(
498      */
GlobalScreenshot(Context context)499     public GlobalScreenshot(Context context) {
500         Resources r = context.getResources();
501         mContext = context;
502         LayoutInflater layoutInflater = (LayoutInflater)
503                 context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
504 
505         // Inflate the screenshot layout
506         mScreenshotLayout = layoutInflater.inflate(R.layout.global_screenshot, null);
507         mBackgroundView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_background);
508         mScreenshotView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot);
509         mScreenshotFlash = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_flash);
510         mScreenshotSelectorView = (ScreenshotSelectorView) mScreenshotLayout.findViewById(
511                 R.id.global_screenshot_selector);
512         mScreenshotLayout.setFocusable(true);
513         mScreenshotSelectorView.setFocusable(true);
514         mScreenshotSelectorView.setFocusableInTouchMode(true);
515         mScreenshotLayout.setOnTouchListener(new View.OnTouchListener() {
516             @Override
517             public boolean onTouch(View v, MotionEvent event) {
518                 // Intercept and ignore all touch events
519                 return true;
520             }
521         });
522 
523         // Setup the window that we are going to use
524         mWindowLayoutParams = new WindowManager.LayoutParams(
525                 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0,
526                 WindowManager.LayoutParams.TYPE_SCREENSHOT,
527                 WindowManager.LayoutParams.FLAG_FULLSCREEN
528                     | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
529                     | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED,
530                 PixelFormat.TRANSLUCENT);
531         mWindowLayoutParams.setTitle("ScreenshotAnimation");
532         mWindowLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
533         mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
534         mNotificationManager =
535             (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
536         mDisplay = mWindowManager.getDefaultDisplay();
537         mDisplayMetrics = new DisplayMetrics();
538         mDisplay.getRealMetrics(mDisplayMetrics);
539 
540         // Get the various target sizes
541         mNotificationIconSize =
542             r.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
543 
544         // Scale has to account for both sides of the bg
545         mBgPadding = (float) r.getDimensionPixelSize(R.dimen.global_screenshot_bg_padding);
546         mBgPaddingScale = mBgPadding /  mDisplayMetrics.widthPixels;
547 
548         // determine the optimal preview size
549         int panelWidth = 0;
550         try {
551             panelWidth = r.getDimensionPixelSize(R.dimen.notification_panel_width);
552         } catch (Resources.NotFoundException e) {
553         }
554         if (panelWidth <= 0) {
555             // includes notification_panel_width==match_parent (-1)
556             panelWidth = mDisplayMetrics.widthPixels;
557         }
558         mPreviewWidth = panelWidth;
559         mPreviewHeight = r.getDimensionPixelSize(R.dimen.notification_max_height);
560 
561         // Setup the Camera shutter sound
562         mCameraSound = new MediaActionSound();
563         mCameraSound.load(MediaActionSound.SHUTTER_CLICK);
564     }
565 
566     /**
567      * Creates a new worker thread and saves the screenshot to the media store.
568      */
saveScreenshotInWorkerThread(Runnable finisher)569     private void saveScreenshotInWorkerThread(Runnable finisher) {
570         SaveImageInBackgroundData data = new SaveImageInBackgroundData();
571         data.context = mContext;
572         data.image = mScreenBitmap;
573         data.iconSize = mNotificationIconSize;
574         data.finisher = finisher;
575         data.previewWidth = mPreviewWidth;
576         data.previewheight = mPreviewHeight;
577         if (mSaveInBgTask != null) {
578             mSaveInBgTask.cancel(false);
579         }
580         mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data, mNotificationManager)
581                 .execute();
582     }
583 
584     /**
585      * Takes a screenshot of the current display and shows an animation.
586      */
takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible, Rect crop)587     private void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible,
588             Rect crop) {
589         int rot = mDisplay.getRotation();
590         int width = crop.width();
591         int height = crop.height();
592 
593         // Take the screenshot
594         mScreenBitmap = SurfaceControl.screenshot(crop, width, height, rot);
595         if (mScreenBitmap == null) {
596             notifyScreenshotError(mContext, mNotificationManager,
597                     R.string.screenshot_failed_to_capture_text);
598             finisher.run();
599             return;
600         }
601 
602         // Optimizations
603         mScreenBitmap.setHasAlpha(false);
604         mScreenBitmap.prepareToDraw();
605 
606         // Start the post-screenshot animation
607         startAnimation(finisher, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels,
608                 statusBarVisible, navBarVisible);
609     }
610 
takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible)611     void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible) {
612         mDisplay.getRealMetrics(mDisplayMetrics);
613         takeScreenshot(finisher, statusBarVisible, navBarVisible,
614                 new Rect(0, 0, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels));
615     }
616 
617     /**
618      * Displays a screenshot selector
619      */
takeScreenshotPartial(final Runnable finisher, final boolean statusBarVisible, final boolean navBarVisible)620     void takeScreenshotPartial(final Runnable finisher, final boolean statusBarVisible,
621             final boolean navBarVisible) {
622         mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams);
623         mScreenshotSelectorView.setOnTouchListener(new View.OnTouchListener() {
624             @Override
625             public boolean onTouch(View v, MotionEvent event) {
626                 ScreenshotSelectorView view = (ScreenshotSelectorView) v;
627                 switch (event.getAction()) {
628                     case MotionEvent.ACTION_DOWN:
629                         view.startSelection((int) event.getX(), (int) event.getY());
630                         return true;
631                     case MotionEvent.ACTION_MOVE:
632                         view.updateSelection((int) event.getX(), (int) event.getY());
633                         return true;
634                     case MotionEvent.ACTION_UP:
635                         view.setVisibility(View.GONE);
636                         mWindowManager.removeView(mScreenshotLayout);
637                         final Rect rect = view.getSelectionRect();
638                         if (rect != null) {
639                             if (rect.width() != 0 && rect.height() != 0) {
640                                 // Need mScreenshotLayout to handle it after the view disappears
641                                 mScreenshotLayout.post(new Runnable() {
642                                     public void run() {
643                                         takeScreenshot(finisher, statusBarVisible, navBarVisible,
644                                                 rect);
645                                     }
646                                 });
647                             }
648                         }
649 
650                         view.stopSelection();
651                         return true;
652                 }
653 
654                 return false;
655             }
656         });
657         mScreenshotLayout.post(new Runnable() {
658             @Override
659             public void run() {
660                 mScreenshotSelectorView.setVisibility(View.VISIBLE);
661                 mScreenshotSelectorView.requestFocus();
662             }
663         });
664     }
665 
666     /**
667      * Cancels screenshot request
668      */
stopScreenshot()669     void stopScreenshot() {
670         // If the selector layer still presents on screen, we remove it and resets its state.
671         if (mScreenshotSelectorView.getSelectionRect() != null) {
672             mWindowManager.removeView(mScreenshotLayout);
673             mScreenshotSelectorView.stopSelection();
674         }
675     }
676 
677     /**
678      * Starts the animation after taking the screenshot
679      */
startAnimation(final Runnable finisher, int w, int h, boolean statusBarVisible, boolean navBarVisible)680     private void startAnimation(final Runnable finisher, int w, int h, boolean statusBarVisible,
681             boolean navBarVisible) {
682         // If power save is on, show a toast so there is some visual indication that a screenshot
683         // has been taken.
684         PowerManager powerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
685         if (powerManager.isPowerSaveMode()) {
686             Toast.makeText(mContext, R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show();
687         }
688 
689         // Add the view for the animation
690         mScreenshotView.setImageBitmap(mScreenBitmap);
691         mScreenshotLayout.requestFocus();
692 
693         // Setup the animation with the screenshot just taken
694         if (mScreenshotAnimation != null) {
695             if (mScreenshotAnimation.isStarted()) {
696                 mScreenshotAnimation.end();
697             }
698             mScreenshotAnimation.removeAllListeners();
699         }
700 
701         mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams);
702         ValueAnimator screenshotDropInAnim = createScreenshotDropInAnimation();
703         ValueAnimator screenshotFadeOutAnim = createScreenshotDropOutAnimation(w, h,
704                 statusBarVisible, navBarVisible);
705         mScreenshotAnimation = new AnimatorSet();
706         mScreenshotAnimation.playSequentially(screenshotDropInAnim, screenshotFadeOutAnim);
707         mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
708             @Override
709             public void onAnimationEnd(Animator animation) {
710                 // Save the screenshot once we have a bit of time now
711                 saveScreenshotInWorkerThread(finisher);
712                 mWindowManager.removeView(mScreenshotLayout);
713 
714                 // Clear any references to the bitmap
715                 mScreenBitmap = null;
716                 mScreenshotView.setImageBitmap(null);
717             }
718         });
719         mScreenshotLayout.post(new Runnable() {
720             @Override
721             public void run() {
722                 // Play the shutter sound to notify that we've taken a screenshot
723                 mCameraSound.play(MediaActionSound.SHUTTER_CLICK);
724 
725                 mScreenshotView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
726                 mScreenshotView.buildLayer();
727                 mScreenshotAnimation.start();
728             }
729         });
730     }
createScreenshotDropInAnimation()731     private ValueAnimator createScreenshotDropInAnimation() {
732         final float flashPeakDurationPct = ((float) (SCREENSHOT_FLASH_TO_PEAK_DURATION)
733                 / SCREENSHOT_DROP_IN_DURATION);
734         final float flashDurationPct = 2f * flashPeakDurationPct;
735         final Interpolator flashAlphaInterpolator = new Interpolator() {
736             @Override
737             public float getInterpolation(float x) {
738                 // Flash the flash view in and out quickly
739                 if (x <= flashDurationPct) {
740                     return (float) Math.sin(Math.PI * (x / flashDurationPct));
741                 }
742                 return 0;
743             }
744         };
745         final Interpolator scaleInterpolator = new Interpolator() {
746             @Override
747             public float getInterpolation(float x) {
748                 // We start scaling when the flash is at it's peak
749                 if (x < flashPeakDurationPct) {
750                     return 0;
751                 }
752                 return (x - flashDurationPct) / (1f - flashDurationPct);
753             }
754         };
755 
756         Resources r = mContext.getResources();
757         if ((r.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)
758                 == Configuration.UI_MODE_NIGHT_YES) {
759             mScreenshotView.getBackground().setTint(Color.BLACK);
760         } else {
761             mScreenshotView.getBackground().setTintList(null);
762         }
763 
764         ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
765         anim.setDuration(SCREENSHOT_DROP_IN_DURATION);
766         anim.addListener(new AnimatorListenerAdapter() {
767             @Override
768             public void onAnimationStart(Animator animation) {
769                 mBackgroundView.setAlpha(0f);
770                 mBackgroundView.setVisibility(View.VISIBLE);
771                 mScreenshotView.setAlpha(0f);
772                 mScreenshotView.setTranslationX(0f);
773                 mScreenshotView.setTranslationY(0f);
774                 mScreenshotView.setScaleX(SCREENSHOT_SCALE + mBgPaddingScale);
775                 mScreenshotView.setScaleY(SCREENSHOT_SCALE + mBgPaddingScale);
776                 mScreenshotView.setVisibility(View.VISIBLE);
777                 mScreenshotFlash.setAlpha(0f);
778                 mScreenshotFlash.setVisibility(View.VISIBLE);
779             }
780             @Override
781             public void onAnimationEnd(android.animation.Animator animation) {
782                 mScreenshotFlash.setVisibility(View.GONE);
783             }
784         });
785         anim.addUpdateListener(new AnimatorUpdateListener() {
786             @Override
787             public void onAnimationUpdate(ValueAnimator animation) {
788                 float t = (Float) animation.getAnimatedValue();
789                 float scaleT = (SCREENSHOT_SCALE + mBgPaddingScale)
790                     - scaleInterpolator.getInterpolation(t)
791                         * (SCREENSHOT_SCALE - SCREENSHOT_DROP_IN_MIN_SCALE);
792                 mBackgroundView.setAlpha(scaleInterpolator.getInterpolation(t) * BACKGROUND_ALPHA);
793                 mScreenshotView.setAlpha(t);
794                 mScreenshotView.setScaleX(scaleT);
795                 mScreenshotView.setScaleY(scaleT);
796                 mScreenshotFlash.setAlpha(flashAlphaInterpolator.getInterpolation(t));
797             }
798         });
799         return anim;
800     }
createScreenshotDropOutAnimation(int w, int h, boolean statusBarVisible, boolean navBarVisible)801     private ValueAnimator createScreenshotDropOutAnimation(int w, int h, boolean statusBarVisible,
802             boolean navBarVisible) {
803         ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
804         anim.setStartDelay(SCREENSHOT_DROP_OUT_DELAY);
805         anim.addListener(new AnimatorListenerAdapter() {
806             @Override
807             public void onAnimationEnd(Animator animation) {
808                 mBackgroundView.setVisibility(View.GONE);
809                 mScreenshotView.setVisibility(View.GONE);
810                 mScreenshotView.setLayerType(View.LAYER_TYPE_NONE, null);
811             }
812         });
813 
814         if (!statusBarVisible || !navBarVisible) {
815             // There is no status bar/nav bar, so just fade the screenshot away in place
816             anim.setDuration(SCREENSHOT_FAST_DROP_OUT_DURATION);
817             anim.addUpdateListener(new AnimatorUpdateListener() {
818                 @Override
819                 public void onAnimationUpdate(ValueAnimator animation) {
820                     float t = (Float) animation.getAnimatedValue();
821                     float scaleT = (SCREENSHOT_DROP_IN_MIN_SCALE + mBgPaddingScale)
822                             - t * (SCREENSHOT_DROP_IN_MIN_SCALE - SCREENSHOT_FAST_DROP_OUT_MIN_SCALE);
823                     mBackgroundView.setAlpha((1f - t) * BACKGROUND_ALPHA);
824                     mScreenshotView.setAlpha(1f - t);
825                     mScreenshotView.setScaleX(scaleT);
826                     mScreenshotView.setScaleY(scaleT);
827                 }
828             });
829         } else {
830             // In the case where there is a status bar, animate to the origin of the bar (top-left)
831             final float scaleDurationPct = (float) SCREENSHOT_DROP_OUT_SCALE_DURATION
832                     / SCREENSHOT_DROP_OUT_DURATION;
833             final Interpolator scaleInterpolator = new Interpolator() {
834                 @Override
835                 public float getInterpolation(float x) {
836                     if (x < scaleDurationPct) {
837                         // Decelerate, and scale the input accordingly
838                         return (float) (1f - Math.pow(1f - (x / scaleDurationPct), 2f));
839                     }
840                     return 1f;
841                 }
842             };
843 
844             // Determine the bounds of how to scale
845             float halfScreenWidth = (w - 2f * mBgPadding) / 2f;
846             float halfScreenHeight = (h - 2f * mBgPadding) / 2f;
847             final float offsetPct = SCREENSHOT_DROP_OUT_MIN_SCALE_OFFSET;
848             final PointF finalPos = new PointF(
849                 -halfScreenWidth + (SCREENSHOT_DROP_OUT_MIN_SCALE + offsetPct) * halfScreenWidth,
850                 -halfScreenHeight + (SCREENSHOT_DROP_OUT_MIN_SCALE + offsetPct) * halfScreenHeight);
851 
852             // Animate the screenshot to the status bar
853             anim.setDuration(SCREENSHOT_DROP_OUT_DURATION);
854             anim.addUpdateListener(new AnimatorUpdateListener() {
855                 @Override
856                 public void onAnimationUpdate(ValueAnimator animation) {
857                     float t = (Float) animation.getAnimatedValue();
858                     float scaleT = (SCREENSHOT_DROP_IN_MIN_SCALE + mBgPaddingScale)
859                         - scaleInterpolator.getInterpolation(t)
860                             * (SCREENSHOT_DROP_IN_MIN_SCALE - SCREENSHOT_DROP_OUT_MIN_SCALE);
861                     mBackgroundView.setAlpha((1f - t) * BACKGROUND_ALPHA);
862                     mScreenshotView.setAlpha(1f - scaleInterpolator.getInterpolation(t));
863                     mScreenshotView.setScaleX(scaleT);
864                     mScreenshotView.setScaleY(scaleT);
865                     mScreenshotView.setTranslationX(t * finalPos.x);
866                     mScreenshotView.setTranslationY(t * finalPos.y);
867                 }
868             });
869         }
870         return anim;
871     }
872 
notifyScreenshotError(Context context, NotificationManager nManager, int msgResId)873     static void notifyScreenshotError(Context context, NotificationManager nManager, int msgResId) {
874         Resources r = context.getResources();
875         String errorMsg = r.getString(msgResId);
876 
877         // Repurpose the existing notification to notify the user of the error
878         Notification.Builder b = new Notification.Builder(context, NotificationChannels.ALERTS)
879             .setTicker(r.getString(R.string.screenshot_failed_title))
880             .setContentTitle(r.getString(R.string.screenshot_failed_title))
881             .setContentText(errorMsg)
882             .setSmallIcon(R.drawable.stat_notify_image_error)
883             .setWhen(System.currentTimeMillis())
884             .setVisibility(Notification.VISIBILITY_PUBLIC) // ok to show outside lockscreen
885             .setCategory(Notification.CATEGORY_ERROR)
886             .setAutoCancel(true)
887             .setColor(context.getColor(
888                         com.android.internal.R.color.system_notification_accent_color));
889         final DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
890                 Context.DEVICE_POLICY_SERVICE);
891         final Intent intent = dpm.createAdminSupportIntent(
892                 DevicePolicyManager.POLICY_DISABLE_SCREEN_CAPTURE);
893         if (intent != null) {
894             final PendingIntent pendingIntent = PendingIntent.getActivityAsUser(
895                     context, 0, intent, 0, null, UserHandle.CURRENT);
896             b.setContentIntent(pendingIntent);
897         }
898 
899         SystemUI.overrideNotificationAppName(context, b, true);
900 
901         Notification n = new Notification.BigTextStyle(b)
902                 .bigText(errorMsg)
903                 .build();
904         nManager.notify(SystemMessage.NOTE_GLOBAL_SCREENSHOT, n);
905     }
906 
907     /**
908      * Receiver to proxy the share or edit intent, used to clean up the notification and send
909      * appropriate signals to the system (ie. to dismiss the keyguard if necessary).
910      */
911     public static class ActionProxyReceiver extends BroadcastReceiver {
912         static final int CLOSE_WINDOWS_TIMEOUT_MILLIS = 3000;
913 
914         @Override
onReceive(Context context, final Intent intent)915         public void onReceive(Context context, final Intent intent) {
916             Runnable startActivityRunnable = () -> {
917                 try {
918                     ActivityManagerWrapper.getInstance().closeSystemWindows(
919                             SYSTEM_DIALOG_REASON_SCREENSHOT).get(
920                             CLOSE_WINDOWS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
921                 } catch (TimeoutException | InterruptedException | ExecutionException e) {
922                     Slog.e(TAG, "Unable to share screenshot", e);
923                     return;
924                 }
925 
926                 Intent actionIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT);
927                 if (intent.getBooleanExtra(EXTRA_CANCEL_NOTIFICATION, false)) {
928                     cancelScreenshotNotification(context);
929                 }
930                 ActivityOptions opts = ActivityOptions.makeBasic();
931                 opts.setDisallowEnterPictureInPictureWhileLaunching(
932                         intent.getBooleanExtra(EXTRA_DISALLOW_ENTER_PIP, false));
933                 context.startActivityAsUser(actionIntent, opts.toBundle(), UserHandle.CURRENT);
934             };
935             StatusBar statusBar = SysUiServiceProvider.getComponent(context, StatusBar.class);
936             statusBar.executeRunnableDismissingKeyguard(startActivityRunnable, null,
937                     true /* dismissShade */, true /* afterKeyguardGone */, true /* deferred */);
938         }
939     }
940 
941     /**
942      * Removes the notification for a screenshot after a share target is chosen.
943      */
944     public static class TargetChosenReceiver extends BroadcastReceiver {
945         @Override
onReceive(Context context, Intent intent)946         public void onReceive(Context context, Intent intent) {
947             // Clear the notification only after the user has chosen a share action
948             cancelScreenshotNotification(context);
949         }
950     }
951 
952     /**
953      * Removes the last screenshot.
954      */
955     public static class DeleteScreenshotReceiver extends BroadcastReceiver {
956         @Override
onReceive(Context context, Intent intent)957         public void onReceive(Context context, Intent intent) {
958             if (!intent.hasExtra(SCREENSHOT_URI_ID)) {
959                 return;
960             }
961 
962             // Clear the notification when the image is deleted
963             cancelScreenshotNotification(context);
964 
965             // And delete the image from the media store
966             final Uri uri = Uri.parse(intent.getStringExtra(SCREENSHOT_URI_ID));
967             new DeleteImageInBackgroundTask(context).execute(uri);
968         }
969     }
970 
cancelScreenshotNotification(Context context)971     private static void cancelScreenshotNotification(Context context) {
972         final NotificationManager nm =
973                 (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
974         nm.cancel(SystemMessage.NOTE_GLOBAL_SCREENSHOT);
975     }
976 }
977