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