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