1 /*
2  * Copyright (C) 2019 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 
21 import android.app.Notification;
22 import android.app.NotificationManager;
23 import android.app.PendingIntent;
24 import android.app.admin.DevicePolicyManager;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.res.Resources;
28 import android.graphics.Bitmap;
29 import android.graphics.Canvas;
30 import android.graphics.ColorMatrix;
31 import android.graphics.ColorMatrixColorFilter;
32 import android.graphics.Matrix;
33 import android.graphics.Paint;
34 import android.graphics.Picture;
35 import android.os.UserHandle;
36 import android.util.DisplayMetrics;
37 import android.view.WindowManager;
38 
39 import com.android.internal.messages.nano.SystemMessageProto;
40 import com.android.systemui.R;
41 import com.android.systemui.SystemUI;
42 import com.android.systemui.util.NotificationChannels;
43 
44 import javax.inject.Inject;
45 
46 /**
47  * Convenience class to handle showing and hiding notifications while taking a screenshot.
48  */
49 public class ScreenshotNotificationsController {
50     private static final String TAG = "ScreenshotNotificationManager";
51 
52     private final Context mContext;
53     private final Resources mResources;
54     private final NotificationManager mNotificationManager;
55     private final Notification.BigPictureStyle mNotificationStyle;
56 
57     private int mIconSize;
58     private int mPreviewWidth, mPreviewHeight;
59     private Notification.Builder mNotificationBuilder, mPublicNotificationBuilder;
60 
61     @Inject
ScreenshotNotificationsController(Context context, WindowManager windowManager)62     ScreenshotNotificationsController(Context context, WindowManager windowManager) {
63         mContext = context;
64         mResources = context.getResources();
65 
66         mNotificationManager =
67                 (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
68 
69         mIconSize = mResources.getDimensionPixelSize(
70                 android.R.dimen.notification_large_icon_height);
71 
72         DisplayMetrics displayMetrics = new DisplayMetrics();
73         windowManager.getDefaultDisplay().getRealMetrics(displayMetrics);
74 
75 
76         // determine the optimal preview size
77         int panelWidth = 0;
78         try {
79             panelWidth = mResources.getDimensionPixelSize(R.dimen.notification_panel_width);
80         } catch (Resources.NotFoundException e) {
81         }
82         if (panelWidth <= 0) {
83             // includes notification_panel_width==match_parent (-1)
84             panelWidth = displayMetrics.widthPixels;
85         }
86         mPreviewWidth = panelWidth;
87         mPreviewHeight = mResources.getDimensionPixelSize(R.dimen.notification_max_height);
88 
89         // Setup the notification
90         mNotificationStyle = new Notification.BigPictureStyle();
91     }
92 
93     /**
94      * Resets the notification builders.
95      */
reset()96     public void reset() {
97         // The public notification will show similar info but with the actual screenshot omitted
98         mPublicNotificationBuilder =
99                 new Notification.Builder(mContext, NotificationChannels.SCREENSHOTS_HEADSUP);
100         mNotificationBuilder =
101                 new Notification.Builder(mContext, NotificationChannels.SCREENSHOTS_HEADSUP);
102     }
103 
104     /**
105      * Sets the current screenshot bitmap.
106      *
107      * @param image the bitmap of the current screenshot (used for preview)
108      */
setImage(Bitmap image)109     public void setImage(Bitmap image) {
110         // Create the large notification icon
111         int imageWidth = image.getWidth();
112         int imageHeight = image.getHeight();
113 
114         Paint paint = new Paint();
115         ColorMatrix desat = new ColorMatrix();
116         desat.setSaturation(0.25f);
117         paint.setColorFilter(new ColorMatrixColorFilter(desat));
118         Matrix matrix = new Matrix();
119         int overlayColor = 0x40FFFFFF;
120 
121         matrix.setTranslate((mPreviewWidth - imageWidth) / 2f, (mPreviewHeight - imageHeight) / 2f);
122 
123         Bitmap picture = generateAdjustedHwBitmap(
124                 image, mPreviewWidth, mPreviewHeight, matrix, paint, overlayColor);
125 
126         mNotificationStyle.bigPicture(picture.createAshmemBitmap());
127 
128         // Note, we can't use the preview for the small icon, since it is non-square
129         float scale = (float) mIconSize / Math.min(imageWidth, imageHeight);
130         matrix.setScale(scale, scale);
131         matrix.postTranslate(
132                 (mIconSize - (scale * imageWidth)) / 2,
133                 (mIconSize - (scale * imageHeight)) / 2);
134         Bitmap icon =
135                 generateAdjustedHwBitmap(image, mIconSize, mIconSize, matrix, paint, overlayColor);
136 
137         /**
138          * NOTE: The following code prepares the notification builder for updating the
139          * notification after the screenshot has been written to disk.
140          */
141 
142         // On the tablet, the large icon makes the notification appear as if it is clickable
143         // (and on small devices, the large icon is not shown) so defer showing the large icon
144         // until we compose the final post-save notification below.
145         mNotificationBuilder.setLargeIcon(icon.createAshmemBitmap());
146         // But we still don't set it for the expanded view, allowing the smallIcon to show here.
147         mNotificationStyle.bigLargeIcon((Bitmap) null);
148     }
149 
150     /**
151      * Shows a notification to inform the user that a screenshot is currently being saved.
152      */
showSavingScreenshotNotification()153     public void showSavingScreenshotNotification() {
154         final long now = System.currentTimeMillis();
155 
156         mPublicNotificationBuilder
157                 .setContentTitle(mResources.getString(R.string.screenshot_saving_title))
158                 .setSmallIcon(R.drawable.stat_notify_image)
159                 .setCategory(Notification.CATEGORY_PROGRESS)
160                 .setWhen(now)
161                 .setShowWhen(true)
162                 .setColor(mResources.getColor(
163                         com.android.internal.R.color.system_notification_accent_color));
164         SystemUI.overrideNotificationAppName(mContext, mPublicNotificationBuilder, true);
165 
166         mNotificationBuilder
167                 .setContentTitle(mResources.getString(R.string.screenshot_saving_title))
168                 .setSmallIcon(R.drawable.stat_notify_image)
169                 .setWhen(now)
170                 .setShowWhen(true)
171                 .setColor(mResources.getColor(
172                         com.android.internal.R.color.system_notification_accent_color))
173                 .setStyle(mNotificationStyle)
174                 .setPublicVersion(mPublicNotificationBuilder.build());
175         mNotificationBuilder.setFlag(Notification.FLAG_NO_CLEAR, true);
176         SystemUI.overrideNotificationAppName(mContext, mNotificationBuilder, true);
177 
178         mNotificationManager.notify(SystemMessageProto.SystemMessage.NOTE_GLOBAL_SCREENSHOT,
179                 mNotificationBuilder.build());
180     }
181 
182     /**
183      * Shows a notification with the saved screenshot and actions that can be taken with it.
184      *
185      * @param actionData SavedImageData struct with image URI and actions
186      */
showScreenshotActionsNotification( GlobalScreenshot.SavedImageData actionData)187     public void showScreenshotActionsNotification(
188             GlobalScreenshot.SavedImageData actionData) {
189         mNotificationBuilder.addAction(actionData.shareAction);
190         mNotificationBuilder.addAction(actionData.editAction);
191         mNotificationBuilder.addAction(actionData.deleteAction);
192         for (Notification.Action smartAction : actionData.smartActions) {
193             mNotificationBuilder.addAction(smartAction);
194         }
195 
196         // Create the intent to show the screenshot in gallery
197         Intent launchIntent = new Intent(Intent.ACTION_VIEW);
198         launchIntent.setDataAndType(actionData.uri, "image/png");
199         launchIntent.setFlags(
200                 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
201 
202         final long now = System.currentTimeMillis();
203 
204         // Update the text and the icon for the existing notification
205         mPublicNotificationBuilder
206                 .setContentTitle(mResources.getString(R.string.screenshot_saved_title))
207                 .setContentText(mResources.getString(R.string.screenshot_saved_text))
208                 .setContentIntent(PendingIntent.getActivity(mContext, 0, launchIntent, 0))
209                 .setWhen(now)
210                 .setAutoCancel(true)
211                 .setColor(mContext.getColor(
212                         com.android.internal.R.color.system_notification_accent_color));
213         mNotificationBuilder
214                 .setContentTitle(mResources.getString(R.string.screenshot_saved_title))
215                 .setContentText(mResources.getString(R.string.screenshot_saved_text))
216                 .setContentIntent(PendingIntent.getActivity(mContext, 0, launchIntent, 0))
217                 .setWhen(now)
218                 .setAutoCancel(true)
219                 .setColor(mContext.getColor(
220                         com.android.internal.R.color.system_notification_accent_color))
221                 .setPublicVersion(mPublicNotificationBuilder.build())
222                 .setFlag(Notification.FLAG_NO_CLEAR, false);
223 
224         mNotificationManager.notify(SystemMessageProto.SystemMessage.NOTE_GLOBAL_SCREENSHOT,
225                 mNotificationBuilder.build());
226     }
227 
228     /**
229      * Sends a notification that the screenshot capture has failed.
230      */
notifyScreenshotError(int msgResId)231     public void notifyScreenshotError(int msgResId) {
232         Resources res = mContext.getResources();
233         String errorMsg = res.getString(msgResId);
234 
235         // Repurpose the existing notification to notify the user of the error
236         Notification.Builder b = new Notification.Builder(mContext, NotificationChannels.ALERTS)
237                 .setTicker(res.getString(R.string.screenshot_failed_title))
238                 .setContentTitle(res.getString(R.string.screenshot_failed_title))
239                 .setContentText(errorMsg)
240                 .setSmallIcon(R.drawable.stat_notify_image_error)
241                 .setWhen(System.currentTimeMillis())
242                 .setVisibility(Notification.VISIBILITY_PUBLIC) // ok to show outside lockscreen
243                 .setCategory(Notification.CATEGORY_ERROR)
244                 .setAutoCancel(true)
245                 .setColor(mContext.getColor(
246                         com.android.internal.R.color.system_notification_accent_color));
247         final DevicePolicyManager dpm =
248                 (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
249         final Intent intent =
250                 dpm.createAdminSupportIntent(DevicePolicyManager.POLICY_DISABLE_SCREEN_CAPTURE);
251         if (intent != null) {
252             final PendingIntent pendingIntent = PendingIntent.getActivityAsUser(
253                     mContext, 0, intent, 0, null, UserHandle.CURRENT);
254             b.setContentIntent(pendingIntent);
255         }
256 
257         SystemUI.overrideNotificationAppName(mContext, b, true);
258 
259         Notification n = new Notification.BigTextStyle(b)
260                 .bigText(errorMsg)
261                 .build();
262         mNotificationManager.notify(SystemMessageProto.SystemMessage.NOTE_GLOBAL_SCREENSHOT, n);
263     }
264 
265     /**
266      * Cancels the current screenshot notification.
267      */
cancelNotification()268     public void cancelNotification() {
269         mNotificationManager.cancel(SystemMessageProto.SystemMessage.NOTE_GLOBAL_SCREENSHOT);
270     }
271 
272     /**
273      * Generates a new hardware bitmap with specified values, copying the content from the
274      * passed in bitmap.
275      */
generateAdjustedHwBitmap(Bitmap bitmap, int width, int height, Matrix matrix, Paint paint, int color)276     private Bitmap generateAdjustedHwBitmap(Bitmap bitmap, int width, int height, Matrix matrix,
277             Paint paint, int color) {
278         Picture picture = new Picture();
279         Canvas canvas = picture.beginRecording(width, height);
280         canvas.drawColor(color);
281         canvas.drawBitmap(bitmap, matrix, paint);
282         picture.endRecording();
283         return Bitmap.createBitmap(picture);
284     }
285 
cancelScreenshotNotification(Context context)286     static void cancelScreenshotNotification(Context context) {
287         final NotificationManager nm =
288                 (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
289         nm.cancel(SystemMessageProto.SystemMessage.NOTE_GLOBAL_SCREENSHOT);
290     }
291 }
292