GlobalScreenshot.java revision a63bb84bbe98e72871c2138ab3eb517d0f9a80ef
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
17package com.android.systemui.screenshot;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ValueAnimator;
23import android.animation.ValueAnimator.AnimatorUpdateListener;
24import android.app.Notification;
25import android.app.NotificationManager;
26import android.app.PendingIntent;
27import android.content.ContentResolver;
28import android.content.ContentValues;
29import android.content.Context;
30import android.content.Intent;
31import android.content.res.Resources;
32import android.graphics.Bitmap;
33import android.graphics.Canvas;
34import android.graphics.Matrix;
35import android.graphics.PixelFormat;
36import android.graphics.PointF;
37import android.graphics.RectF;
38import android.net.Uri;
39import android.os.AsyncTask;
40import android.os.Environment;
41import android.os.ServiceManager;
42import android.provider.MediaStore;
43import android.util.DisplayMetrics;
44import android.view.Display;
45import android.view.IWindowManager;
46import android.view.LayoutInflater;
47import android.view.MotionEvent;
48import android.view.Surface;
49import android.view.View;
50import android.view.ViewGroup;
51import android.view.WindowManager;
52import android.view.animation.AccelerateInterpolator;
53import android.view.animation.DecelerateInterpolator;
54import android.widget.FrameLayout;
55import android.widget.ImageView;
56
57import com.android.server.wm.WindowManagerService;
58import com.android.systemui.R;
59
60import java.io.File;
61import java.io.OutputStream;
62import java.text.SimpleDateFormat;
63import java.util.Date;
64
65/**
66 * POD used in the AsyncTask which saves an image in the background.
67 */
68class SaveImageInBackgroundData {
69    Context context;
70    Bitmap image;
71    Runnable finisher;
72    int iconSize;
73    int result;
74}
75
76/**
77 * An AsyncTask that saves an image to the media store in the background.
78 */
79class SaveImageInBackgroundTask extends AsyncTask<SaveImageInBackgroundData, Void,
80        SaveImageInBackgroundData> {
81    private static final String TAG = "SaveImageInBackgroundTask";
82    private static final String SCREENSHOTS_DIR_NAME = "Screenshots";
83    private static final String SCREENSHOT_FILE_NAME_TEMPLATE = "Screenshot_%s.png";
84    private static final String SCREENSHOT_FILE_PATH_TEMPLATE = "%s/%s/%s";
85
86    private int mNotificationId;
87    private NotificationManager mNotificationManager;
88    private Notification.Builder mNotificationBuilder;
89    private Intent mLaunchIntent;
90    private String mImageDir;
91    private String mImageFileName;
92    private String mImageFilePath;
93    private String mImageDate;
94    private long mImageTime;
95
96    // WORKAROUND: We want the same notification across screenshots that we update so that we don't
97    // spam a user's notification drawer.  However, we only show the ticker for the saving state
98    // and if the ticker text is the same as the previous notification, then it will not show. So
99    // for now, we just add and remove a space from the ticker text to trigger the animation when
100    // necessary.
101    private static boolean mTickerAddSpace;
102
103    SaveImageInBackgroundTask(Context context, SaveImageInBackgroundData data,
104            NotificationManager nManager, int nId) {
105        Resources r = context.getResources();
106
107        // Prepare all the output metadata
108        mImageTime = System.currentTimeMillis();
109        mImageDate = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date(mImageTime));
110        mImageDir = Environment.getExternalStoragePublicDirectory(
111                Environment.DIRECTORY_PICTURES).getAbsolutePath();
112        mImageFileName = String.format(SCREENSHOT_FILE_NAME_TEMPLATE, mImageDate);
113        mImageFilePath = String.format(SCREENSHOT_FILE_PATH_TEMPLATE, mImageDir,
114                SCREENSHOTS_DIR_NAME, mImageFileName);
115
116        // Create the large notification icon
117        int imageWidth = data.image.getWidth();
118        int imageHeight = data.image.getHeight();
119        int iconWidth = data.iconSize;
120        int iconHeight = data.iconSize;
121        if (imageWidth > imageHeight) {
122            iconWidth = (int) (((float) iconHeight / imageHeight) * imageWidth);
123        } else {
124            iconHeight = (int) (((float) iconWidth / imageWidth) * imageHeight);
125        }
126        Bitmap rawIcon = Bitmap.createScaledBitmap(data.image, iconWidth, iconHeight, true);
127        Bitmap croppedIcon = Bitmap.createBitmap(rawIcon, (iconWidth - data.iconSize) / 2,
128                (iconHeight - data.iconSize) / 2, data.iconSize, data.iconSize);
129
130        // Show the intermediate notification
131        mLaunchIntent = new Intent(Intent.ACTION_VIEW);
132        mLaunchIntent.setDataAndType(Uri.fromFile(new File(mImageFilePath)), "image/png");
133        mLaunchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
134        mTickerAddSpace = !mTickerAddSpace;
135        mNotificationId = nId;
136        mNotificationBuilder = new Notification.Builder(context)
137            .setLargeIcon(croppedIcon)
138            .setTicker(r.getString(R.string.screenshot_saving_ticker)
139                    + (mTickerAddSpace ? " " : ""))
140            .setContentTitle(r.getString(R.string.screenshot_saving_title))
141            .setContentText(r.getString(R.string.screenshot_saving_text))
142            .setSmallIcon(android.R.drawable.ic_menu_gallery)
143            .setWhen(System.currentTimeMillis());
144        Notification n = mNotificationBuilder.getNotification();
145        n.flags |= Notification.FLAG_NO_CLEAR;
146
147        mNotificationManager = nManager;
148        mNotificationManager.notify(nId, n);
149    }
150
151    @Override
152    protected SaveImageInBackgroundData doInBackground(SaveImageInBackgroundData... params) {
153        if (params.length != 1) return null;
154
155        Context context = params[0].context;
156        Bitmap image = params[0].image;
157
158        try {
159            // Save the screenshot to the MediaStore
160            ContentValues values = new ContentValues();
161            ContentResolver resolver = context.getContentResolver();
162            values.put(MediaStore.Images.ImageColumns.DATA, mImageFilePath);
163            values.put(MediaStore.Images.ImageColumns.TITLE, mImageFileName);
164            values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, mImageFileName);
165            values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, mImageTime);
166            values.put(MediaStore.Images.ImageColumns.DATE_ADDED, mImageTime);
167            values.put(MediaStore.Images.ImageColumns.DATE_MODIFIED, mImageTime);
168            values.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/png");
169            Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
170
171            OutputStream out = resolver.openOutputStream(uri);
172            image.compress(Bitmap.CompressFormat.PNG, 100, out);
173            out.flush();
174            out.close();
175
176            // update file size in the database
177            values.clear();
178            values.put(MediaStore.Images.ImageColumns.SIZE, new File(mImageFilePath).length());
179            resolver.update(uri, values, null, null);
180
181            params[0].result = 0;
182        } catch (Exception e) {
183            // IOException/UnsupportedOperationException may be thrown if external storage is not
184            // mounted
185            params[0].result = 1;
186        }
187
188        return params[0];
189    };
190
191    @Override
192    protected void onPostExecute(SaveImageInBackgroundData params) {
193        if (params.result > 0) {
194            // Show a message that we've failed to save the image to disk
195            GlobalScreenshot.notifyScreenshotError(params.context, mNotificationManager);
196        } else {
197            // Show the final notification to indicate screenshot saved
198            Resources r = params.context.getResources();
199
200            mNotificationBuilder
201                .setContentTitle(r.getString(R.string.screenshot_saved_title))
202                .setContentText(r.getString(R.string.screenshot_saved_text))
203                .setContentIntent(PendingIntent.getActivity(params.context, 0, mLaunchIntent, 0))
204                .setWhen(System.currentTimeMillis())
205                .setAutoCancel(true);
206
207            Notification n = mNotificationBuilder.getNotification();
208            n.flags &= ~Notification.FLAG_NO_CLEAR;
209            mNotificationManager.notify(mNotificationId, n);
210        }
211        params.finisher.run();
212    };
213}
214
215/**
216 * TODO:
217 *   - Performance when over gl surfaces? Ie. Gallery
218 *   - what do we say in the Toast? Which icon do we get if the user uses another
219 *     type of gallery?
220 */
221class GlobalScreenshot {
222    private static final String TAG = "GlobalScreenshot";
223    private static final int SCREENSHOT_NOTIFICATION_ID = 789;
224    private static final int SCREENSHOT_FADE_IN_DURATION = 250;
225    private static final int SCREENSHOT_FADE_OUT_DELAY = 750;
226    private static final int SCREENSHOT_FADE_OUT_DURATION = 500;
227    private static final int SCREENSHOT_FAST_FADE_OUT_DURATION = 350;
228    private static final float BACKGROUND_ALPHA = 0.65f;
229    private static final float SCREENSHOT_SCALE_FUDGE = 0.075f; // To account for the border padding
230    private static final float SCREENSHOT_SCALE = 0.55f;
231    private static final float SCREENSHOT_FADE_IN_MIN_SCALE = SCREENSHOT_SCALE * 0.975f;
232    private static final float SCREENSHOT_FADE_OUT_MIN_SCALE = SCREENSHOT_SCALE * 0.925f;
233
234    private Context mContext;
235    private LayoutInflater mLayoutInflater;
236    private IWindowManager mIWindowManager;
237    private WindowManager mWindowManager;
238    private WindowManager.LayoutParams mWindowLayoutParams;
239    private NotificationManager mNotificationManager;
240    private Display mDisplay;
241    private DisplayMetrics mDisplayMetrics;
242    private Matrix mDisplayMatrix;
243
244    private Bitmap mScreenBitmap;
245    private View mScreenshotLayout;
246    private ImageView mBackgroundView;
247    private FrameLayout mScreenshotContainerView;
248    private ImageView mScreenshotView;
249
250    private AnimatorSet mScreenshotAnimation;
251
252    private int mStatusBarIconSize;
253    private int mNotificationIconSize;
254    private float mDropOffsetX;
255    private float mDropOffsetY;
256    private float mBgPadding;
257    private float mBgPaddingScale;
258
259
260    /**
261     * @param context everything needs a context :(
262     */
263    public GlobalScreenshot(Context context) {
264        Resources r = context.getResources();
265        mContext = context;
266        mLayoutInflater = (LayoutInflater)
267                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
268
269        // Inflate the screenshot layout
270        mDisplayMatrix = new Matrix();
271        mScreenshotLayout = mLayoutInflater.inflate(R.layout.global_screenshot, null);
272        mBackgroundView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_background);
273        mScreenshotContainerView = (FrameLayout) mScreenshotLayout.findViewById(R.id.global_screenshot_container);
274        mScreenshotView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot);
275        mScreenshotLayout.setFocusable(true);
276        mScreenshotLayout.setOnTouchListener(new View.OnTouchListener() {
277            @Override
278            public boolean onTouch(View v, MotionEvent event) {
279                // Intercept and ignore all touch events
280                return true;
281            }
282        });
283
284        // Setup the window that we are going to use
285        mIWindowManager = IWindowManager.Stub.asInterface(
286                ServiceManager.getService(Context.WINDOW_SERVICE));
287        mWindowLayoutParams = new WindowManager.LayoutParams(
288                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0,
289                WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY,
290                WindowManager.LayoutParams.FLAG_FULLSCREEN
291                    | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
292                    | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
293                    | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED,
294                PixelFormat.TRANSLUCENT);
295        mWindowLayoutParams.setTitle("ScreenshotAnimation");
296        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
297        mNotificationManager =
298            (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
299        mDisplay = mWindowManager.getDefaultDisplay();
300        mDisplayMetrics = new DisplayMetrics();
301        mDisplay.getRealMetrics(mDisplayMetrics);
302
303        // Get the various target sizes
304        mStatusBarIconSize =
305            r.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size);
306        mNotificationIconSize =
307            r.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
308        mDropOffsetX = r.getDimensionPixelSize(R.dimen.global_screenshot_drop_offset_x);
309        mDropOffsetY = r.getDimensionPixelSize(R.dimen.global_screenshot_drop_offset_y);
310
311        // Scale has to account for both sides of the bg
312        mBgPadding = (float) r.getDimensionPixelSize(R.dimen.global_screenshot_bg_padding);
313        mBgPaddingScale = (2f * mBgPadding) /  mDisplayMetrics.widthPixels;
314    }
315
316    /**
317     * Creates a new worker thread and saves the screenshot to the media store.
318     */
319    private void saveScreenshotInWorkerThread(Runnable finisher) {
320        SaveImageInBackgroundData data = new SaveImageInBackgroundData();
321        data.context = mContext;
322        data.image = mScreenBitmap;
323        data.iconSize = mNotificationIconSize;
324        data.finisher = finisher;
325        new SaveImageInBackgroundTask(mContext, data, mNotificationManager,
326                SCREENSHOT_NOTIFICATION_ID).execute(data);
327    }
328
329    /**
330     * @return the current display rotation in degrees
331     */
332    private float getDegreesForRotation(int value) {
333        switch (value) {
334        case Surface.ROTATION_90:
335            return 360f - 90f;
336        case Surface.ROTATION_180:
337            return 360f - 180f;
338        case Surface.ROTATION_270:
339            return 360f - 270f;
340        }
341        return 0f;
342    }
343
344    /**
345     * Takes a screenshot of the current display and shows an animation.
346     */
347    void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible) {
348        // We need to orient the screenshot correctly (and the Surface api seems to take screenshots
349        // only in the natural orientation of the device :!)
350        mDisplay.getRealMetrics(mDisplayMetrics);
351        float[] dims = {mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels};
352        float degrees = getDegreesForRotation(mDisplay.getRotation());
353        boolean requiresRotation = (degrees > 0);
354        if (requiresRotation) {
355            // Get the dimensions of the device in its native orientation
356            mDisplayMatrix.reset();
357            mDisplayMatrix.preRotate(-degrees);
358            mDisplayMatrix.mapPoints(dims);
359            dims[0] = Math.abs(dims[0]);
360            dims[1] = Math.abs(dims[1]);
361        }
362        mScreenBitmap = Surface.screenshot((int) dims[0], (int) dims[1]);
363        if (requiresRotation) {
364            // Rotate the screenshot to the current orientation
365            Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels,
366                    mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888);
367            Canvas c = new Canvas(ss);
368            c.translate(ss.getWidth() / 2, ss.getHeight() / 2);
369            c.rotate(degrees);
370            c.translate(-dims[0] / 2, -dims[1] / 2);
371            c.drawBitmap(mScreenBitmap, 0, 0, null);
372            c.setBitmap(null);
373            mScreenBitmap = ss;
374        }
375
376        // If we couldn't take the screenshot, notify the user
377        if (mScreenBitmap == null) {
378            notifyScreenshotError(mContext, mNotificationManager);
379            finisher.run();
380            return;
381        }
382
383        // Optimizations
384        mScreenBitmap.setHasAlpha(false);
385        mScreenBitmap.prepareToDraw();
386
387        // Start the post-screenshot animation
388        startAnimation(finisher, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels,
389                statusBarVisible, navBarVisible);
390    }
391
392
393    /**
394     * Starts the animation after taking the screenshot
395     */
396    private void startAnimation(final Runnable finisher, int w, int h, boolean statusBarVisible,
397            boolean navBarVisible) {
398        // Add the view for the animation
399        mScreenshotView.setImageBitmap(mScreenBitmap);
400        mScreenshotLayout.requestFocus();
401
402        // Setup the animation with the screenshot just taken
403        if (mScreenshotAnimation != null) {
404            mScreenshotAnimation.end();
405        }
406
407        mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams);
408        ValueAnimator screenshotFadeInAnim = createScreenshotFadeInAnimation();
409        ValueAnimator screenshotFadeOutAnim = createScreenshotFadeOutAnimation(w, h,
410                statusBarVisible, navBarVisible);
411        mScreenshotAnimation = new AnimatorSet();
412        mScreenshotAnimation.play(screenshotFadeInAnim).before(screenshotFadeOutAnim);
413        mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
414            @Override
415            public void onAnimationEnd(Animator animation) {
416                // Save the screenshot once we have a bit of time now
417                saveScreenshotInWorkerThread(finisher);
418                mWindowManager.removeView(mScreenshotLayout);
419            }
420        });
421        mScreenshotLayout.post(new Runnable() {
422            @Override
423            public void run() {
424                mScreenshotContainerView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
425                mScreenshotContainerView.buildLayer();
426                mScreenshotAnimation.start();
427            }
428        });
429    }
430    private ValueAnimator createScreenshotFadeInAnimation() {
431        ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
432        anim.setInterpolator(new AccelerateInterpolator(1.5f));
433        anim.setDuration(SCREENSHOT_FADE_IN_DURATION);
434        anim.addListener(new AnimatorListenerAdapter() {
435            @Override
436            public void onAnimationStart(Animator animation) {
437                mBackgroundView.setAlpha(0f);
438                mBackgroundView.setVisibility(View.VISIBLE);
439                mScreenshotContainerView.setTranslationX(0f);
440                mScreenshotContainerView.setTranslationY(0f);
441                mScreenshotContainerView.setScaleX(SCREENSHOT_FADE_IN_MIN_SCALE);
442                mScreenshotContainerView.setScaleY(SCREENSHOT_FADE_IN_MIN_SCALE);
443                mScreenshotContainerView.setAlpha(0f);
444                mScreenshotContainerView.setVisibility(View.VISIBLE);
445            }
446        });
447        anim.addUpdateListener(new AnimatorUpdateListener() {
448            @Override
449            public void onAnimationUpdate(ValueAnimator animation) {
450                float t = ((Float) animation.getAnimatedValue()).floatValue();
451                float scaleT = (SCREENSHOT_FADE_IN_MIN_SCALE)
452                    + (float) t * (SCREENSHOT_SCALE - SCREENSHOT_FADE_IN_MIN_SCALE);
453                mBackgroundView.setAlpha(t * BACKGROUND_ALPHA);
454                mScreenshotContainerView.setScaleX(scaleT);
455                mScreenshotContainerView.setScaleY(scaleT);
456                mScreenshotContainerView.setAlpha(t);
457            }
458        });
459        return anim;
460    }
461    private ValueAnimator createScreenshotFadeOutAnimation(int w, int h, boolean statusBarVisible,
462            boolean navBarVisible) {
463        ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
464        anim.setInterpolator(new DecelerateInterpolator(0.5f));
465        anim.setStartDelay(SCREENSHOT_FADE_OUT_DELAY);
466        anim.addListener(new AnimatorListenerAdapter() {
467            @Override
468            public void onAnimationEnd(Animator animation) {
469                mBackgroundView.setVisibility(View.GONE);
470                mScreenshotContainerView.setVisibility(View.GONE);
471                mScreenshotContainerView.setLayerType(View.LAYER_TYPE_NONE, null);
472            }
473        });
474
475        if (!statusBarVisible || !navBarVisible) {
476            // There is no status bar/nav bar, so just fade the screenshot away in place
477            anim.setDuration(SCREENSHOT_FAST_FADE_OUT_DURATION);
478            anim.addUpdateListener(new AnimatorUpdateListener() {
479                @Override
480                public void onAnimationUpdate(ValueAnimator animation) {
481                    float t = ((Float) animation.getAnimatedValue()).floatValue();
482                    float scaleT = (SCREENSHOT_FADE_OUT_MIN_SCALE)
483                            + (float) (1f - t) * (SCREENSHOT_SCALE - SCREENSHOT_FADE_OUT_MIN_SCALE);
484                    mBackgroundView.setAlpha((1f - t) * BACKGROUND_ALPHA);
485                    mScreenshotContainerView.setAlpha((1f - t) * BACKGROUND_ALPHA);
486                    mScreenshotContainerView.setScaleX(scaleT);
487                    mScreenshotContainerView.setScaleY(scaleT);
488                }
489            });
490        } else {
491            // Determine the bounds of how to scale
492            float halfScreenWidth = (w - 2f * mBgPadding) / 2f;
493            float halfScreenHeight = (h - 2f * mBgPadding) / 2f;
494            final RectF finalBounds = new RectF(mDropOffsetX, mDropOffsetY,
495                    mDropOffsetX + mStatusBarIconSize,
496                    mDropOffsetY + mStatusBarIconSize);
497            final PointF currentPos = new PointF(0f, 0f);
498            final PointF finalPos = new PointF(-halfScreenWidth + finalBounds.centerX(),
499                    -halfScreenHeight + finalBounds.centerY());
500            final DecelerateInterpolator d = new DecelerateInterpolator(2f);
501            // Note: since the scale origin is in the center of the view, divide difference by 2
502            float tmpMinScale = 0f;
503            if (w > h) {
504                tmpMinScale = finalBounds.width() / (2f * w);
505            } else {
506                tmpMinScale = finalBounds.height() / (2f * h);
507            }
508            final float minScale = tmpMinScale;
509
510            // Animate the screenshot to the status bar
511            anim.setDuration(SCREENSHOT_FADE_OUT_DURATION);
512            anim.addUpdateListener(new AnimatorUpdateListener() {
513                @Override
514                public void onAnimationUpdate(ValueAnimator animation) {
515                    float t = ((Float) animation.getAnimatedValue()).floatValue();
516                    float scaleT = minScale
517                            + (float) (1f - t) * (SCREENSHOT_SCALE - minScale - mBgPaddingScale)
518                            + mBgPaddingScale;
519                    mScreenshotContainerView.setAlpha(d.getInterpolation(1f - t));
520                    mScreenshotContainerView.setTranslationX(d.getInterpolation(t) * finalPos.x);
521                    mScreenshotContainerView.setTranslationY(d.getInterpolation(t) * finalPos.y);
522                    mScreenshotContainerView.setScaleX(scaleT);
523                    mScreenshotContainerView.setScaleY(scaleT);
524                    mBackgroundView.setAlpha((1f - t) * BACKGROUND_ALPHA);
525                }
526            });
527        }
528        return anim;
529    }
530
531    static void notifyScreenshotError(Context context, NotificationManager nManager) {
532        Resources r = context.getResources();
533
534        // Clear all existing notification, compose the new notification and show it
535        Notification n = new Notification.Builder(context)
536            .setTicker(r.getString(R.string.screenshot_failed_title))
537            .setContentTitle(r.getString(R.string.screenshot_failed_title))
538            .setContentText(r.getString(R.string.screenshot_failed_text))
539            .setSmallIcon(android.R.drawable.ic_menu_report_image)
540            .setWhen(System.currentTimeMillis())
541            .setAutoCancel(true)
542            .getNotification();
543        nManager.notify(SCREENSHOT_NOTIFICATION_ID, n);
544    }
545}
546