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