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 android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.ValueAnimator; 23 import android.animation.ValueAnimator.AnimatorUpdateListener; 24 import android.app.Notification; 25 import android.app.Notification.BigPictureStyle; 26 import android.app.NotificationManager; 27 import android.app.PendingIntent; 28 import android.content.ContentResolver; 29 import android.content.ContentValues; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.res.Resources; 33 import android.graphics.Bitmap; 34 import android.graphics.Canvas; 35 import android.graphics.ColorMatrix; 36 import android.graphics.ColorMatrixColorFilter; 37 import android.graphics.Matrix; 38 import android.graphics.Paint; 39 import android.graphics.PixelFormat; 40 import android.graphics.PointF; 41 import android.media.MediaActionSound; 42 import android.net.Uri; 43 import android.os.AsyncTask; 44 import android.os.Environment; 45 import android.os.Process; 46 import android.provider.MediaStore; 47 import android.util.DisplayMetrics; 48 import android.view.Display; 49 import android.view.LayoutInflater; 50 import android.view.MotionEvent; 51 import android.view.Surface; 52 import android.view.SurfaceControl; 53 import android.view.View; 54 import android.view.ViewGroup; 55 import android.view.WindowManager; 56 import android.view.animation.Interpolator; 57 import android.widget.ImageView; 58 59 import com.android.systemui.R; 60 61 import java.io.File; 62 import java.io.OutputStream; 63 import java.text.DateFormat; 64 import java.text.SimpleDateFormat; 65 import java.util.Date; 66 67 /** 68 * POD used in the AsyncTask which saves an image in the background. 69 */ 70 class SaveImageInBackgroundData { 71 Context context; 72 Bitmap image; 73 Uri imageUri; 74 Runnable finisher; 75 int iconSize; 76 int result; 77 int previewWidth; 78 int previewheight; 79 clearImage()80 void clearImage() { 81 image = null; 82 imageUri = null; 83 iconSize = 0; 84 } clearContext()85 void clearContext() { 86 context = null; 87 } 88 } 89 90 /** 91 * An AsyncTask that saves an image to the media store in the background. 92 */ 93 class SaveImageInBackgroundTask extends AsyncTask<SaveImageInBackgroundData, Void, 94 SaveImageInBackgroundData> { 95 private static final String TAG = "SaveImageInBackgroundTask"; 96 97 private static final String SCREENSHOTS_DIR_NAME = "Screenshots"; 98 private static final String SCREENSHOT_FILE_NAME_TEMPLATE = "Screenshot_%s.png"; 99 private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)"; 100 101 private final int mNotificationId; 102 private final NotificationManager mNotificationManager; 103 private final Notification.Builder mNotificationBuilder, mPublicNotificationBuilder; 104 private final File mScreenshotDir; 105 private final String mImageFileName; 106 private final String mImageFilePath; 107 private final long mImageTime; 108 private final BigPictureStyle mNotificationStyle; 109 private final int mImageWidth; 110 private final int mImageHeight; 111 112 // WORKAROUND: We want the same notification across screenshots that we update so that we don't 113 // spam a user's notification drawer. However, we only show the ticker for the saving state 114 // and if the ticker text is the same as the previous notification, then it will not show. So 115 // for now, we just add and remove a space from the ticker text to trigger the animation when 116 // necessary. 117 private static boolean mTickerAddSpace; 118 SaveImageInBackgroundTask(Context context, SaveImageInBackgroundData data, NotificationManager nManager, int nId)119 SaveImageInBackgroundTask(Context context, SaveImageInBackgroundData data, 120 NotificationManager nManager, int nId) { 121 Resources r = context.getResources(); 122 123 // Prepare all the output metadata 124 mImageTime = System.currentTimeMillis(); 125 String imageDate = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date(mImageTime)); 126 mImageFileName = String.format(SCREENSHOT_FILE_NAME_TEMPLATE, imageDate); 127 128 mScreenshotDir = new File(Environment.getExternalStoragePublicDirectory( 129 Environment.DIRECTORY_PICTURES), SCREENSHOTS_DIR_NAME); 130 mImageFilePath = new File(mScreenshotDir, mImageFileName).getAbsolutePath(); 131 132 // Create the large notification icon 133 mImageWidth = data.image.getWidth(); 134 mImageHeight = data.image.getHeight(); 135 int iconSize = data.iconSize; 136 int previewWidth = data.previewWidth; 137 int previewHeight = data.previewheight; 138 139 final int shortSide = mImageWidth < mImageHeight ? mImageWidth : mImageHeight; 140 Bitmap preview = Bitmap.createBitmap(previewWidth, previewHeight, data.image.getConfig()); 141 Canvas c = new Canvas(preview); 142 Paint paint = new Paint(); 143 ColorMatrix desat = new ColorMatrix(); 144 desat.setSaturation(0.25f); 145 paint.setColorFilter(new ColorMatrixColorFilter(desat)); 146 Matrix matrix = new Matrix(); 147 matrix.postTranslate((previewWidth - mImageWidth) / 2, 148 (previewHeight - mImageHeight) / 2); 149 c.drawBitmap(data.image, matrix, paint); 150 c.drawColor(0x40FFFFFF); 151 c.setBitmap(null); 152 153 Bitmap croppedIcon = Bitmap.createScaledBitmap(preview, iconSize, iconSize, true); 154 155 // Show the intermediate notification 156 mTickerAddSpace = !mTickerAddSpace; 157 mNotificationId = nId; 158 mNotificationManager = nManager; 159 final long now = System.currentTimeMillis(); 160 161 mNotificationBuilder = new Notification.Builder(context) 162 .setTicker(r.getString(R.string.screenshot_saving_ticker) 163 + (mTickerAddSpace ? " " : "")) 164 .setContentTitle(r.getString(R.string.screenshot_saving_title)) 165 .setContentText(r.getString(R.string.screenshot_saving_text)) 166 .setSmallIcon(R.drawable.stat_notify_image) 167 .setWhen(now) 168 .setColor(r.getColor(com.android.internal.R.color.system_notification_accent_color)); 169 170 mNotificationStyle = new Notification.BigPictureStyle() 171 .bigPicture(preview); 172 mNotificationBuilder.setStyle(mNotificationStyle); 173 174 // For "public" situations we want to show all the same info but 175 // omit the actual screenshot image. 176 mPublicNotificationBuilder = new Notification.Builder(context) 177 .setContentTitle(r.getString(R.string.screenshot_saving_title)) 178 .setContentText(r.getString(R.string.screenshot_saving_text)) 179 .setSmallIcon(R.drawable.stat_notify_image) 180 .setCategory(Notification.CATEGORY_PROGRESS) 181 .setWhen(now) 182 .setColor(r.getColor( 183 com.android.internal.R.color.system_notification_accent_color)); 184 185 mNotificationBuilder.setPublicVersion(mPublicNotificationBuilder.build()); 186 187 Notification n = mNotificationBuilder.build(); 188 n.flags |= Notification.FLAG_NO_CLEAR; 189 mNotificationManager.notify(nId, n); 190 191 // On the tablet, the large icon makes the notification appear as if it is clickable (and 192 // on small devices, the large icon is not shown) so defer showing the large icon until 193 // we compose the final post-save notification below. 194 mNotificationBuilder.setLargeIcon(croppedIcon); 195 // But we still don't set it for the expanded view, allowing the smallIcon to show here. 196 mNotificationStyle.bigLargeIcon(null); 197 } 198 199 @Override 200 protected SaveImageInBackgroundData doInBackground(SaveImageInBackgroundData... params) { 201 if (params.length != 1) return null; 202 if (isCancelled()) { 203 params[0].clearImage(); 204 params[0].clearContext(); 205 return null; 206 } 207 208 // By default, AsyncTask sets the worker thread to have background thread priority, so bump 209 // it back up so that we save a little quicker. 210 Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND); 211 212 Context context = params[0].context; 213 Bitmap image = params[0].image; 214 Resources r = context.getResources(); 215 216 try { 217 // Create screenshot directory if it doesn't exist 218 mScreenshotDir.mkdirs(); 219 220 // media provider uses seconds for DATE_MODIFIED and DATE_ADDED, but milliseconds 221 // for DATE_TAKEN 222 long dateSeconds = mImageTime / 1000; 223 224 // Save the screenshot to the MediaStore 225 ContentValues values = new ContentValues(); 226 ContentResolver resolver = context.getContentResolver(); 227 values.put(MediaStore.Images.ImageColumns.DATA, mImageFilePath); 228 values.put(MediaStore.Images.ImageColumns.TITLE, mImageFileName); 229 values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, mImageFileName); 230 values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, mImageTime); 231 values.put(MediaStore.Images.ImageColumns.DATE_ADDED, dateSeconds); 232 values.put(MediaStore.Images.ImageColumns.DATE_MODIFIED, dateSeconds); 233 values.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/png"); 234 values.put(MediaStore.Images.ImageColumns.WIDTH, mImageWidth); 235 values.put(MediaStore.Images.ImageColumns.HEIGHT, mImageHeight); 236 Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); 237 238 String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime)); 239 String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate); 240 Intent sharingIntent = new Intent(Intent.ACTION_SEND); 241 sharingIntent.setType("image/png"); 242 sharingIntent.putExtra(Intent.EXTRA_STREAM, uri); 243 sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject); 244 245 Intent chooserIntent = Intent.createChooser(sharingIntent, null); 246 chooserIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK 247 | Intent.FLAG_ACTIVITY_NEW_TASK); 248 249 mNotificationBuilder.addAction(R.drawable.ic_menu_share, 250 r.getString(com.android.internal.R.string.share), 251 PendingIntent.getActivity(context, 0, chooserIntent, 252 PendingIntent.FLAG_CANCEL_CURRENT)); 253 254 OutputStream out = resolver.openOutputStream(uri); 255 image.compress(Bitmap.CompressFormat.PNG, 100, out); 256 out.flush(); 257 out.close(); 258 259 // update file size in the database 260 values.clear(); 261 values.put(MediaStore.Images.ImageColumns.SIZE, new File(mImageFilePath).length()); 262 resolver.update(uri, values, null, null); 263 264 params[0].imageUri = uri; 265 params[0].image = null; 266 params[0].result = 0; 267 } catch (Exception e) { 268 // IOException/UnsupportedOperationException may be thrown if external storage is not 269 // mounted 270 params[0].clearImage(); 271 params[0].result = 1; 272 } 273 274 // Recycle the bitmap data 275 if (image != null) { 276 image.recycle(); 277 } 278 279 return params[0]; 280 } 281 282 @Override 283 protected void onPostExecute(SaveImageInBackgroundData params) { 284 if (isCancelled()) { 285 params.finisher.run(); 286 params.clearImage(); 287 params.clearContext(); 288 return; 289 } 290 291 if (params.result > 0) { 292 // Show a message that we've failed to save the image to disk 293 GlobalScreenshot.notifyScreenshotError(params.context, mNotificationManager); 294 } else { 295 // Show the final notification to indicate screenshot saved 296 Resources r = params.context.getResources(); 297 298 // Create the intent to show the screenshot in gallery 299 Intent launchIntent = new Intent(Intent.ACTION_VIEW); 300 launchIntent.setDataAndType(params.imageUri, "image/png"); 301 launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 302 303 final long now = System.currentTimeMillis(); 304 305 mNotificationBuilder 306 .setContentTitle(r.getString(R.string.screenshot_saved_title)) 307 .setContentText(r.getString(R.string.screenshot_saved_text)) 308 .setContentIntent(PendingIntent.getActivity(params.context, 0, launchIntent, 0)) 309 .setWhen(now) 310 .setAutoCancel(true) 311 .setColor(r.getColor( 312 com.android.internal.R.color.system_notification_accent_color));; 313 314 // Update the text in the public version as well 315 mPublicNotificationBuilder 316 .setContentTitle(r.getString(R.string.screenshot_saved_title)) 317 .setContentText(r.getString(R.string.screenshot_saved_text)) 318 .setContentIntent(PendingIntent.getActivity(params.context, 0, launchIntent, 0)) 319 .setWhen(now) 320 .setAutoCancel(true) 321 .setColor(r.getColor( 322 com.android.internal.R.color.system_notification_accent_color)); 323 324 mNotificationBuilder.setPublicVersion(mPublicNotificationBuilder.build()); 325 326 Notification n = mNotificationBuilder.build(); 327 n.flags &= ~Notification.FLAG_NO_CLEAR; 328 mNotificationManager.notify(mNotificationId, n); 329 } 330 params.finisher.run(); 331 params.clearContext(); 332 } 333 } 334 335 /** 336 * TODO: 337 * - Performance when over gl surfaces? Ie. Gallery 338 * - what do we say in the Toast? Which icon do we get if the user uses another 339 * type of gallery? 340 */ 341 class GlobalScreenshot { 342 private static final String TAG = "GlobalScreenshot"; 343 344 private static final int SCREENSHOT_NOTIFICATION_ID = 789; 345 private static final int SCREENSHOT_FLASH_TO_PEAK_DURATION = 130; 346 private static final int SCREENSHOT_DROP_IN_DURATION = 430; 347 private static final int SCREENSHOT_DROP_OUT_DELAY = 500; 348 private static final int SCREENSHOT_DROP_OUT_DURATION = 430; 349 private static final int SCREENSHOT_DROP_OUT_SCALE_DURATION = 370; 350 private static final int SCREENSHOT_FAST_DROP_OUT_DURATION = 320; 351 private static final float BACKGROUND_ALPHA = 0.5f; 352 private static final float SCREENSHOT_SCALE = 1f; 353 private static final float SCREENSHOT_DROP_IN_MIN_SCALE = SCREENSHOT_SCALE * 0.725f; 354 private static final float SCREENSHOT_DROP_OUT_MIN_SCALE = SCREENSHOT_SCALE * 0.45f; 355 private static final float SCREENSHOT_FAST_DROP_OUT_MIN_SCALE = SCREENSHOT_SCALE * 0.6f; 356 private static final float SCREENSHOT_DROP_OUT_MIN_SCALE_OFFSET = 0f; 357 private final int mPreviewWidth; 358 private final int mPreviewHeight; 359 360 private Context mContext; 361 private WindowManager mWindowManager; 362 private WindowManager.LayoutParams mWindowLayoutParams; 363 private NotificationManager mNotificationManager; 364 private Display mDisplay; 365 private DisplayMetrics mDisplayMetrics; 366 private Matrix mDisplayMatrix; 367 368 private Bitmap mScreenBitmap; 369 private View mScreenshotLayout; 370 private ImageView mBackgroundView; 371 private ImageView mScreenshotView; 372 private ImageView mScreenshotFlash; 373 374 private AnimatorSet mScreenshotAnimation; 375 376 private int mNotificationIconSize; 377 private float mBgPadding; 378 private float mBgPaddingScale; 379 380 private AsyncTask<SaveImageInBackgroundData, Void, SaveImageInBackgroundData> mSaveInBgTask; 381 382 private MediaActionSound mCameraSound; 383 384 385 /** 386 * @param context everything needs a context :( 387 */ GlobalScreenshot(Context context)388 public GlobalScreenshot(Context context) { 389 Resources r = context.getResources(); 390 mContext = context; 391 LayoutInflater layoutInflater = (LayoutInflater) 392 context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 393 394 // Inflate the screenshot layout 395 mDisplayMatrix = new Matrix(); 396 mScreenshotLayout = layoutInflater.inflate(R.layout.global_screenshot, null); 397 mBackgroundView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_background); 398 mScreenshotView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot); 399 mScreenshotFlash = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_flash); 400 mScreenshotLayout.setFocusable(true); 401 mScreenshotLayout.setOnTouchListener(new View.OnTouchListener() { 402 @Override 403 public boolean onTouch(View v, MotionEvent event) { 404 // Intercept and ignore all touch events 405 return true; 406 } 407 }); 408 409 // Setup the window that we are going to use 410 mWindowLayoutParams = new WindowManager.LayoutParams( 411 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0, 412 WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY, 413 WindowManager.LayoutParams.FLAG_FULLSCREEN 414 | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED 415 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 416 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED, 417 PixelFormat.TRANSLUCENT); 418 mWindowLayoutParams.setTitle("ScreenshotAnimation"); 419 mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 420 mNotificationManager = 421 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 422 mDisplay = mWindowManager.getDefaultDisplay(); 423 mDisplayMetrics = new DisplayMetrics(); 424 mDisplay.getRealMetrics(mDisplayMetrics); 425 426 // Get the various target sizes 427 mNotificationIconSize = 428 r.getDimensionPixelSize(android.R.dimen.notification_large_icon_height); 429 430 // Scale has to account for both sides of the bg 431 mBgPadding = (float) r.getDimensionPixelSize(R.dimen.global_screenshot_bg_padding); 432 mBgPaddingScale = mBgPadding / mDisplayMetrics.widthPixels; 433 434 // determine the optimal preview size 435 int panelWidth = 0; 436 try { 437 panelWidth = r.getDimensionPixelSize(R.dimen.notification_panel_width); 438 } catch (Resources.NotFoundException e) { 439 } 440 if (panelWidth <= 0) { 441 // includes notification_panel_width==match_parent (-1) 442 panelWidth = mDisplayMetrics.widthPixels; 443 } 444 mPreviewWidth = panelWidth; 445 mPreviewHeight = r.getDimensionPixelSize(R.dimen.notification_max_height); 446 447 // Setup the Camera shutter sound 448 mCameraSound = new MediaActionSound(); 449 mCameraSound.load(MediaActionSound.SHUTTER_CLICK); 450 } 451 452 /** 453 * Creates a new worker thread and saves the screenshot to the media store. 454 */ saveScreenshotInWorkerThread(Runnable finisher)455 private void saveScreenshotInWorkerThread(Runnable finisher) { 456 SaveImageInBackgroundData data = new SaveImageInBackgroundData(); 457 data.context = mContext; 458 data.image = mScreenBitmap; 459 data.iconSize = mNotificationIconSize; 460 data.finisher = finisher; 461 data.previewWidth = mPreviewWidth; 462 data.previewheight = mPreviewHeight; 463 if (mSaveInBgTask != null) { 464 mSaveInBgTask.cancel(false); 465 } 466 mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data, mNotificationManager, 467 SCREENSHOT_NOTIFICATION_ID).execute(data); 468 } 469 470 /** 471 * @return the current display rotation in degrees 472 */ getDegreesForRotation(int value)473 private float getDegreesForRotation(int value) { 474 switch (value) { 475 case Surface.ROTATION_90: 476 return 360f - 90f; 477 case Surface.ROTATION_180: 478 return 360f - 180f; 479 case Surface.ROTATION_270: 480 return 360f - 270f; 481 } 482 return 0f; 483 } 484 485 /** 486 * Takes a screenshot of the current display and shows an animation. 487 */ takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible)488 void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible) { 489 // We need to orient the screenshot correctly (and the Surface api seems to take screenshots 490 // only in the natural orientation of the device :!) 491 mDisplay.getRealMetrics(mDisplayMetrics); 492 float[] dims = {mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels}; 493 float degrees = getDegreesForRotation(mDisplay.getRotation()); 494 boolean requiresRotation = (degrees > 0); 495 if (requiresRotation) { 496 // Get the dimensions of the device in its native orientation 497 mDisplayMatrix.reset(); 498 mDisplayMatrix.preRotate(-degrees); 499 mDisplayMatrix.mapPoints(dims); 500 dims[0] = Math.abs(dims[0]); 501 dims[1] = Math.abs(dims[1]); 502 } 503 504 // Take the screenshot 505 mScreenBitmap = SurfaceControl.screenshot((int) dims[0], (int) dims[1]); 506 if (mScreenBitmap == null) { 507 notifyScreenshotError(mContext, mNotificationManager); 508 finisher.run(); 509 return; 510 } 511 512 if (requiresRotation) { 513 // Rotate the screenshot to the current orientation 514 Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels, 515 mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888); 516 Canvas c = new Canvas(ss); 517 c.translate(ss.getWidth() / 2, ss.getHeight() / 2); 518 c.rotate(degrees); 519 c.translate(-dims[0] / 2, -dims[1] / 2); 520 c.drawBitmap(mScreenBitmap, 0, 0, null); 521 c.setBitmap(null); 522 // Recycle the previous bitmap 523 mScreenBitmap.recycle(); 524 mScreenBitmap = ss; 525 } 526 527 // Optimizations 528 mScreenBitmap.setHasAlpha(false); 529 mScreenBitmap.prepareToDraw(); 530 531 // Start the post-screenshot animation 532 startAnimation(finisher, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels, 533 statusBarVisible, navBarVisible); 534 } 535 536 537 /** 538 * Starts the animation after taking the screenshot 539 */ startAnimation(final Runnable finisher, int w, int h, boolean statusBarVisible, boolean navBarVisible)540 private void startAnimation(final Runnable finisher, int w, int h, boolean statusBarVisible, 541 boolean navBarVisible) { 542 // Add the view for the animation 543 mScreenshotView.setImageBitmap(mScreenBitmap); 544 mScreenshotLayout.requestFocus(); 545 546 // Setup the animation with the screenshot just taken 547 if (mScreenshotAnimation != null) { 548 mScreenshotAnimation.end(); 549 mScreenshotAnimation.removeAllListeners(); 550 } 551 552 mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); 553 ValueAnimator screenshotDropInAnim = createScreenshotDropInAnimation(); 554 ValueAnimator screenshotFadeOutAnim = createScreenshotDropOutAnimation(w, h, 555 statusBarVisible, navBarVisible); 556 mScreenshotAnimation = new AnimatorSet(); 557 mScreenshotAnimation.playSequentially(screenshotDropInAnim, screenshotFadeOutAnim); 558 mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { 559 @Override 560 public void onAnimationEnd(Animator animation) { 561 // Save the screenshot once we have a bit of time now 562 saveScreenshotInWorkerThread(finisher); 563 mWindowManager.removeView(mScreenshotLayout); 564 565 // Clear any references to the bitmap 566 mScreenBitmap = null; 567 mScreenshotView.setImageBitmap(null); 568 } 569 }); 570 mScreenshotLayout.post(new Runnable() { 571 @Override 572 public void run() { 573 // Play the shutter sound to notify that we've taken a screenshot 574 mCameraSound.play(MediaActionSound.SHUTTER_CLICK); 575 576 mScreenshotView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 577 mScreenshotView.buildLayer(); 578 mScreenshotAnimation.start(); 579 } 580 }); 581 } createScreenshotDropInAnimation()582 private ValueAnimator createScreenshotDropInAnimation() { 583 final float flashPeakDurationPct = ((float) (SCREENSHOT_FLASH_TO_PEAK_DURATION) 584 / SCREENSHOT_DROP_IN_DURATION); 585 final float flashDurationPct = 2f * flashPeakDurationPct; 586 final Interpolator flashAlphaInterpolator = new Interpolator() { 587 @Override 588 public float getInterpolation(float x) { 589 // Flash the flash view in and out quickly 590 if (x <= flashDurationPct) { 591 return (float) Math.sin(Math.PI * (x / flashDurationPct)); 592 } 593 return 0; 594 } 595 }; 596 final Interpolator scaleInterpolator = new Interpolator() { 597 @Override 598 public float getInterpolation(float x) { 599 // We start scaling when the flash is at it's peak 600 if (x < flashPeakDurationPct) { 601 return 0; 602 } 603 return (x - flashDurationPct) / (1f - flashDurationPct); 604 } 605 }; 606 ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); 607 anim.setDuration(SCREENSHOT_DROP_IN_DURATION); 608 anim.addListener(new AnimatorListenerAdapter() { 609 @Override 610 public void onAnimationStart(Animator animation) { 611 mBackgroundView.setAlpha(0f); 612 mBackgroundView.setVisibility(View.VISIBLE); 613 mScreenshotView.setAlpha(0f); 614 mScreenshotView.setTranslationX(0f); 615 mScreenshotView.setTranslationY(0f); 616 mScreenshotView.setScaleX(SCREENSHOT_SCALE + mBgPaddingScale); 617 mScreenshotView.setScaleY(SCREENSHOT_SCALE + mBgPaddingScale); 618 mScreenshotView.setVisibility(View.VISIBLE); 619 mScreenshotFlash.setAlpha(0f); 620 mScreenshotFlash.setVisibility(View.VISIBLE); 621 } 622 @Override 623 public void onAnimationEnd(android.animation.Animator animation) { 624 mScreenshotFlash.setVisibility(View.GONE); 625 } 626 }); 627 anim.addUpdateListener(new AnimatorUpdateListener() { 628 @Override 629 public void onAnimationUpdate(ValueAnimator animation) { 630 float t = (Float) animation.getAnimatedValue(); 631 float scaleT = (SCREENSHOT_SCALE + mBgPaddingScale) 632 - scaleInterpolator.getInterpolation(t) 633 * (SCREENSHOT_SCALE - SCREENSHOT_DROP_IN_MIN_SCALE); 634 mBackgroundView.setAlpha(scaleInterpolator.getInterpolation(t) * BACKGROUND_ALPHA); 635 mScreenshotView.setAlpha(t); 636 mScreenshotView.setScaleX(scaleT); 637 mScreenshotView.setScaleY(scaleT); 638 mScreenshotFlash.setAlpha(flashAlphaInterpolator.getInterpolation(t)); 639 } 640 }); 641 return anim; 642 } createScreenshotDropOutAnimation(int w, int h, boolean statusBarVisible, boolean navBarVisible)643 private ValueAnimator createScreenshotDropOutAnimation(int w, int h, boolean statusBarVisible, 644 boolean navBarVisible) { 645 ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); 646 anim.setStartDelay(SCREENSHOT_DROP_OUT_DELAY); 647 anim.addListener(new AnimatorListenerAdapter() { 648 @Override 649 public void onAnimationEnd(Animator animation) { 650 mBackgroundView.setVisibility(View.GONE); 651 mScreenshotView.setVisibility(View.GONE); 652 mScreenshotView.setLayerType(View.LAYER_TYPE_NONE, null); 653 } 654 }); 655 656 if (!statusBarVisible || !navBarVisible) { 657 // There is no status bar/nav bar, so just fade the screenshot away in place 658 anim.setDuration(SCREENSHOT_FAST_DROP_OUT_DURATION); 659 anim.addUpdateListener(new AnimatorUpdateListener() { 660 @Override 661 public void onAnimationUpdate(ValueAnimator animation) { 662 float t = (Float) animation.getAnimatedValue(); 663 float scaleT = (SCREENSHOT_DROP_IN_MIN_SCALE + mBgPaddingScale) 664 - t * (SCREENSHOT_DROP_IN_MIN_SCALE - SCREENSHOT_FAST_DROP_OUT_MIN_SCALE); 665 mBackgroundView.setAlpha((1f - t) * BACKGROUND_ALPHA); 666 mScreenshotView.setAlpha(1f - t); 667 mScreenshotView.setScaleX(scaleT); 668 mScreenshotView.setScaleY(scaleT); 669 } 670 }); 671 } else { 672 // In the case where there is a status bar, animate to the origin of the bar (top-left) 673 final float scaleDurationPct = (float) SCREENSHOT_DROP_OUT_SCALE_DURATION 674 / SCREENSHOT_DROP_OUT_DURATION; 675 final Interpolator scaleInterpolator = new Interpolator() { 676 @Override 677 public float getInterpolation(float x) { 678 if (x < scaleDurationPct) { 679 // Decelerate, and scale the input accordingly 680 return (float) (1f - Math.pow(1f - (x / scaleDurationPct), 2f)); 681 } 682 return 1f; 683 } 684 }; 685 686 // Determine the bounds of how to scale 687 float halfScreenWidth = (w - 2f * mBgPadding) / 2f; 688 float halfScreenHeight = (h - 2f * mBgPadding) / 2f; 689 final float offsetPct = SCREENSHOT_DROP_OUT_MIN_SCALE_OFFSET; 690 final PointF finalPos = new PointF( 691 -halfScreenWidth + (SCREENSHOT_DROP_OUT_MIN_SCALE + offsetPct) * halfScreenWidth, 692 -halfScreenHeight + (SCREENSHOT_DROP_OUT_MIN_SCALE + offsetPct) * halfScreenHeight); 693 694 // Animate the screenshot to the status bar 695 anim.setDuration(SCREENSHOT_DROP_OUT_DURATION); 696 anim.addUpdateListener(new AnimatorUpdateListener() { 697 @Override 698 public void onAnimationUpdate(ValueAnimator animation) { 699 float t = (Float) animation.getAnimatedValue(); 700 float scaleT = (SCREENSHOT_DROP_IN_MIN_SCALE + mBgPaddingScale) 701 - scaleInterpolator.getInterpolation(t) 702 * (SCREENSHOT_DROP_IN_MIN_SCALE - SCREENSHOT_DROP_OUT_MIN_SCALE); 703 mBackgroundView.setAlpha((1f - t) * BACKGROUND_ALPHA); 704 mScreenshotView.setAlpha(1f - scaleInterpolator.getInterpolation(t)); 705 mScreenshotView.setScaleX(scaleT); 706 mScreenshotView.setScaleY(scaleT); 707 mScreenshotView.setTranslationX(t * finalPos.x); 708 mScreenshotView.setTranslationY(t * finalPos.y); 709 } 710 }); 711 } 712 return anim; 713 } 714 notifyScreenshotError(Context context, NotificationManager nManager)715 static void notifyScreenshotError(Context context, NotificationManager nManager) { 716 Resources r = context.getResources(); 717 718 // Clear all existing notification, compose the new notification and show it 719 Notification.Builder b = new Notification.Builder(context) 720 .setTicker(r.getString(R.string.screenshot_failed_title)) 721 .setContentTitle(r.getString(R.string.screenshot_failed_title)) 722 .setContentText(r.getString(R.string.screenshot_failed_text)) 723 .setSmallIcon(R.drawable.stat_notify_image_error) 724 .setWhen(System.currentTimeMillis()) 725 .setVisibility(Notification.VISIBILITY_PUBLIC) // ok to show outside lockscreen 726 .setCategory(Notification.CATEGORY_ERROR) 727 .setAutoCancel(true) 728 .setColor(context.getResources().getColor( 729 com.android.internal.R.color.system_notification_accent_color)); 730 Notification n = 731 new Notification.BigTextStyle(b) 732 .bigText(r.getString(R.string.screenshot_failed_text)) 733 .build(); 734 nManager.notify(SCREENSHOT_NOTIFICATION_ID, n); 735 } 736 } 737