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 package com.android.contacts.activities; 17 18 import android.animation.Animator; 19 import android.animation.AnimatorListenerAdapter; 20 import android.animation.ObjectAnimator; 21 import android.animation.PropertyValuesHolder; 22 import android.app.Activity; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.res.Configuration; 26 import android.graphics.Bitmap; 27 import android.graphics.Rect; 28 import android.net.Uri; 29 import android.os.Bundle; 30 import android.os.Parcelable; 31 import android.support.v4.content.FileProvider; 32 import android.view.View; 33 import android.view.ViewGroup.MarginLayoutParams; 34 import android.widget.FrameLayout.LayoutParams; 35 import android.widget.ImageView; 36 37 import com.android.contacts.common.ContactPhotoManager; 38 import com.android.contacts.ContactSaveService; 39 import com.android.contacts.R; 40 import com.android.contacts.detail.PhotoSelectionHandler; 41 import com.android.contacts.editor.PhotoActionPopup; 42 import com.android.contacts.model.RawContactDeltaList; 43 import com.android.contacts.util.ContactPhotoUtils; 44 import com.android.contacts.util.SchedulingUtils; 45 46 import java.io.File; 47 import java.io.FileNotFoundException; 48 49 50 /** 51 * Popup activity for choosing a contact photo within the Contacts app. 52 */ 53 public class PhotoSelectionActivity extends Activity { 54 55 private static final String TAG = "PhotoSelectionActivity"; 56 57 /** Number of ms for the animation to expand the photo. */ 58 private static final int PHOTO_EXPAND_DURATION = 100; 59 60 /** Number of ms for the animation to contract the photo on activity exit. */ 61 private static final int PHOTO_CONTRACT_DURATION = 50; 62 63 /** Number of ms for the animation to hide the backdrop on finish. */ 64 private static final int BACKDROP_FADEOUT_DURATION = 100; 65 66 /** Key used to persist photo uri. */ 67 private static final String KEY_CURRENT_PHOTO_URI = "currentphotouri"; 68 69 /** Key used to persist whether a sub-activity is currently in progress. */ 70 private static final String KEY_SUB_ACTIVITY_IN_PROGRESS = "subinprogress"; 71 72 /** Intent extra to get the photo URI. */ 73 public static final String PHOTO_URI = "photo_uri"; 74 75 /** Intent extra to get the entity delta list. */ 76 public static final String ENTITY_DELTA_LIST = "entity_delta_list"; 77 78 /** Intent extra to indicate whether the contact is the user's profile. */ 79 public static final String IS_PROFILE = "is_profile"; 80 81 /** Intent extra to indicate whether the contact is from a directory (non-editable). */ 82 public static final String IS_DIRECTORY_CONTACT = "is_directory_contact"; 83 84 /** 85 * Intent extra to indicate whether the photo should be animated to show the full contents of 86 * the photo (on a larger portion of the screen) when clicked. If unspecified or false, the 87 * photo will not move from its original location. 88 */ 89 public static final String EXPAND_PHOTO = "expand_photo"; 90 91 /** Source bounds of the image that was clicked on. */ 92 private Rect mSourceBounds; 93 94 /** 95 * The photo URI. May be null, in which case the default avatar will be used. 96 */ 97 private Uri mPhotoUri; 98 99 /** Entity delta list of the contact. */ 100 private RawContactDeltaList mState; 101 102 /** Whether the contact is the user's profile. */ 103 private boolean mIsProfile; 104 105 /** Whether the contact is from a directory. */ 106 private boolean mIsDirectoryContact; 107 108 /** Whether to animate the photo to an expanded view covering more of the screen. */ 109 private boolean mExpandPhoto; 110 111 /** 112 * Side length (in pixels) of the expanded photo if to be expanded. Photos are expected to 113 * be square. 114 */ 115 private int mExpandedPhotoSize; 116 117 /** Height (in pixels) to leave underneath the expanded photo to show the list popup */ 118 private int mHeightOffset; 119 120 /** The semi-transparent backdrop. */ 121 private View mBackdrop; 122 123 /** The photo view. */ 124 private ImageView mPhotoView; 125 126 /** The photo handler attached to this activity, if any. */ 127 private PhotoHandler mPhotoHandler; 128 129 /** Animator to expand the photo out to full size. */ 130 private ObjectAnimator mPhotoAnimator; 131 132 /** Listener for the animation. */ 133 private AnimatorListenerAdapter mAnimationListener; 134 135 /** Whether a change in layout of the photo has occurred that has no animation yet. */ 136 private boolean mAnimationPending; 137 138 /** Prior position of the image (for animating). */ 139 Rect mOriginalPos = new Rect(); 140 141 /** Layout params for the photo view before we started animating. */ 142 private LayoutParams mPhotoStartParams; 143 144 /** Layout params for the photo view after we finished animating. */ 145 private LayoutParams mPhotoEndParams; 146 147 /** Whether a sub-activity is currently in progress. */ 148 private boolean mSubActivityInProgress; 149 150 private boolean mCloseActivityWhenCameBackFromSubActivity; 151 152 /** 153 * A photo result received by the activity, persisted across activity lifecycle. 154 */ 155 private PendingPhotoResult mPendingPhotoResult; 156 157 /** 158 * The photo uri being interacted with, if any. Saved/restored between activity instances. 159 */ 160 private Uri mCurrentPhotoUri; 161 162 @Override 163 protected void onCreate(Bundle savedInstanceState) { 164 super.onCreate(savedInstanceState); 165 setContentView(R.layout.photoselection_activity); 166 if (savedInstanceState != null) { 167 mCurrentPhotoUri = savedInstanceState.getParcelable(KEY_CURRENT_PHOTO_URI); 168 mSubActivityInProgress = savedInstanceState.getBoolean(KEY_SUB_ACTIVITY_IN_PROGRESS); 169 } 170 171 // Pull data out of the intent. 172 final Intent intent = getIntent(); 173 mPhotoUri = intent.getParcelableExtra(PHOTO_URI); 174 mState = (RawContactDeltaList) intent.getParcelableExtra(ENTITY_DELTA_LIST); 175 mIsProfile = intent.getBooleanExtra(IS_PROFILE, false); 176 mIsDirectoryContact = intent.getBooleanExtra(IS_DIRECTORY_CONTACT, false); 177 mExpandPhoto = intent.getBooleanExtra(EXPAND_PHOTO, false); 178 179 // Pull out photo expansion properties from resources 180 mExpandedPhotoSize = getResources().getDimensionPixelSize( 181 R.dimen.detail_contact_photo_expanded_size); 182 mHeightOffset = getResources().getDimensionPixelOffset( 183 R.dimen.expanded_photo_height_offset); 184 185 mBackdrop = findViewById(R.id.backdrop); 186 mPhotoView = (ImageView) findViewById(R.id.photo); 187 mSourceBounds = intent.getSourceBounds(); 188 189 // Fade in the background. 190 animateInBackground(); 191 192 // Dismiss the dialog on clicking the backdrop. 193 mBackdrop.setOnClickListener(new View.OnClickListener() { 194 @Override 195 public void onClick(View v) { 196 finish(); 197 } 198 }); 199 200 // Wait until the layout pass to show the photo, so that the source bounds will match up. 201 SchedulingUtils.doAfterLayout(mBackdrop, new Runnable() { 202 @Override 203 public void run() { 204 displayPhoto(); 205 } 206 }); 207 } 208 209 /** 210 * Compute the adjusted expanded photo size to fit within the enclosing view with the same 211 * aspect ratio. 212 * @param enclosingView This is the view that the photo must fit within. 213 * @param heightOffset This is the amount of height to leave open for the photo action popup. 214 */ 215 private int getAdjustedExpandedPhotoSize(View enclosingView, int heightOffset) { 216 // pull out the bounds of the backdrop 217 final Rect bounds = new Rect(); 218 enclosingView.getDrawingRect(bounds); 219 final int boundsWidth = bounds.width(); 220 final int boundsHeight = bounds.height() - heightOffset; 221 222 // ensure that the new expanded photo size can fit within the backdrop 223 final float alpha = Math.min((float) boundsHeight / (float) mExpandedPhotoSize, 224 (float) boundsWidth / (float) mExpandedPhotoSize); 225 if (alpha < 1.0f) { 226 // need to shrink width and height while maintaining aspect ratio 227 return (int) (alpha * mExpandedPhotoSize); 228 } else { 229 return mExpandedPhotoSize; 230 } 231 } 232 233 @Override 234 public void onConfigurationChanged(Configuration newConfig) { 235 super.onConfigurationChanged(newConfig); 236 237 // The current look may not seem right on the new configuration, so let's just close self. 238 239 if (!mSubActivityInProgress) { 240 finishImmediatelyWithNoAnimation(); 241 } else { 242 // A sub-activity is in progress, so don't close it yet, but close it when we come back 243 // to this activity. 244 mCloseActivityWhenCameBackFromSubActivity = true; 245 } 246 } 247 248 @Override 249 public void finish() { 250 if (!mSubActivityInProgress) { 251 closePhotoAndFinish(); 252 } else { 253 finishImmediatelyWithNoAnimation(); 254 } 255 } 256 257 /** 258 * Builds a well-formed intent for invoking this activity. 259 * @param context The context. 260 * @param photoUri The URI of the current photo (may be null, in which case the default 261 * avatar image will be displayed). 262 * @param photoBitmap The bitmap of the current photo (may be null, in which case the default 263 * avatar image will be displayed). 264 * @param photoBytes The bytes for the current photo (may be null, in which case the default 265 * avatar image will be displayed). 266 * @param photoBounds The pixel bounds of the current photo. 267 * @param delta The entity delta list for the contact. 268 * @param isProfile Whether the contact is the user's profile. 269 * @param isDirectoryContact Whether the contact comes from a directory (non-editable). 270 * @param expandPhotoOnClick Whether the photo should be expanded on click or not (generally, 271 * this should be true for phones, and false for tablets). 272 * @return An intent that can be used to invoke the photo selection activity. 273 */ 274 public static Intent buildIntent(Context context, Uri photoUri, Bitmap photoBitmap, 275 byte[] photoBytes, Rect photoBounds, RawContactDeltaList delta, boolean isProfile, 276 boolean isDirectoryContact, boolean expandPhotoOnClick) { 277 Intent intent = new Intent(context, PhotoSelectionActivity.class); 278 if (photoUri != null && photoBitmap != null && photoBytes != null) { 279 intent.putExtra(PHOTO_URI, photoUri); 280 } 281 intent.setSourceBounds(photoBounds); 282 intent.putExtra(ENTITY_DELTA_LIST, (Parcelable) delta); 283 intent.putExtra(IS_PROFILE, isProfile); 284 intent.putExtra(IS_DIRECTORY_CONTACT, isDirectoryContact); 285 intent.putExtra(EXPAND_PHOTO, expandPhotoOnClick); 286 return intent; 287 } 288 289 private void finishImmediatelyWithNoAnimation() { 290 super.finish(); 291 } 292 293 @Override 294 protected void onDestroy() { 295 super.onDestroy(); 296 if (mPhotoAnimator != null) { 297 mPhotoAnimator.cancel(); 298 mPhotoAnimator = null; 299 } 300 if (mPhotoHandler != null) { 301 mPhotoHandler.destroy(); 302 mPhotoHandler = null; 303 } 304 } 305 306 private void displayPhoto() { 307 // Animate the photo view into its end location. 308 final int[] pos = new int[2]; 309 mBackdrop.getLocationOnScreen(pos); 310 LayoutParams layoutParams = new LayoutParams(mSourceBounds.width(), 311 mSourceBounds.height()); 312 mOriginalPos.left = mSourceBounds.left - pos[0]; 313 mOriginalPos.top = mSourceBounds.top - pos[1]; 314 mOriginalPos.right = mOriginalPos.left + mSourceBounds.width(); 315 mOriginalPos.bottom = mOriginalPos.top + mSourceBounds.height(); 316 layoutParams.setMargins(mOriginalPos.left, mOriginalPos.top, mOriginalPos.right, 317 mOriginalPos.bottom); 318 mPhotoStartParams = layoutParams; 319 mPhotoView.setLayoutParams(layoutParams); 320 mPhotoView.requestLayout(); 321 322 // Load the photo. 323 int photoWidth = getPhotoEndParams().width; 324 if (mPhotoUri != null) { 325 // If we have a URI, the bitmap should be cached directly. 326 ContactPhotoManager.getInstance(this).loadPhoto(mPhotoView, mPhotoUri, photoWidth, 327 false); 328 } else { 329 // Fall back to avatar image. 330 mPhotoView.setImageResource(ContactPhotoManager.getDefaultAvatarResId(this, photoWidth, 331 false)); 332 } 333 334 mPhotoView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 335 @Override 336 public void onLayoutChange(View v, int left, int top, int right, int bottom, 337 int oldLeft, int oldTop, int oldRight, int oldBottom) { 338 if (mAnimationPending) { 339 mAnimationPending = false; 340 PropertyValuesHolder pvhLeft = 341 PropertyValuesHolder.ofInt("left", mOriginalPos.left, left); 342 PropertyValuesHolder pvhTop = 343 PropertyValuesHolder.ofInt("top", mOriginalPos.top, top); 344 PropertyValuesHolder pvhRight = 345 PropertyValuesHolder.ofInt("right", mOriginalPos.right, right); 346 PropertyValuesHolder pvhBottom = 347 PropertyValuesHolder.ofInt("bottom", mOriginalPos.bottom, bottom); 348 ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(mPhotoView, 349 pvhLeft, pvhTop, pvhRight, pvhBottom).setDuration( 350 PHOTO_EXPAND_DURATION); 351 if (mAnimationListener != null) { 352 anim.addListener(mAnimationListener); 353 } 354 anim.start(); 355 } 356 } 357 }); 358 attachPhotoHandler(); 359 } 360 361 /** 362 * This sets the photo's layout params at the end of the animation. 363 * <p> 364 * The scheme is to enlarge the photo to the desired size with the enlarged photo shifted 365 * to the top left of the screen as much as possible while keeping the underlying smaller 366 * photo occluded. 367 */ 368 private LayoutParams getPhotoEndParams() { 369 if (mPhotoEndParams == null) { 370 mPhotoEndParams = new LayoutParams(mPhotoStartParams); 371 if (mExpandPhoto) { 372 final int adjustedPhotoSize = getAdjustedExpandedPhotoSize(mBackdrop, 373 mHeightOffset); 374 int widthDelta = adjustedPhotoSize - mPhotoStartParams.width; 375 int heightDelta = adjustedPhotoSize - mPhotoStartParams.height; 376 if (widthDelta >= 1 || heightDelta >= 1) { 377 // This is an actual expansion. 378 mPhotoEndParams.width = adjustedPhotoSize; 379 mPhotoEndParams.height = adjustedPhotoSize; 380 mPhotoEndParams.topMargin = 381 Math.max(mPhotoStartParams.topMargin - heightDelta, 0); 382 mPhotoEndParams.leftMargin = 383 Math.max(mPhotoStartParams.leftMargin - widthDelta, 0); 384 mPhotoEndParams.bottomMargin = 0; 385 mPhotoEndParams.rightMargin = 0; 386 } 387 } 388 } 389 return mPhotoEndParams; 390 } 391 392 private void animatePhotoOpen() { 393 mAnimationListener = new AnimatorListenerAdapter() { 394 private void capturePhotoPos() { 395 mPhotoView.requestLayout(); 396 mOriginalPos.left = mPhotoView.getLeft(); 397 mOriginalPos.top = mPhotoView.getTop(); 398 mOriginalPos.right = mPhotoView.getRight(); 399 mOriginalPos.bottom = mPhotoView.getBottom(); 400 } 401 402 @Override 403 public void onAnimationEnd(Animator animation) { 404 capturePhotoPos(); 405 if (mPhotoHandler != null) { 406 mPhotoHandler.onClick(mPhotoView); 407 } 408 } 409 410 @Override 411 public void onAnimationCancel(Animator animation) { 412 capturePhotoPos(); 413 } 414 }; 415 animatePhoto(getPhotoEndParams()); 416 } 417 418 private void closePhotoAndFinish() { 419 mAnimationListener = new AnimatorListenerAdapter() { 420 @Override 421 public void onAnimationEnd(Animator animation) { 422 // After the photo animates down, fade it away and finish. 423 ObjectAnimator anim = ObjectAnimator.ofFloat( 424 mPhotoView, "alpha", 0f).setDuration(PHOTO_CONTRACT_DURATION); 425 anim.addListener(new AnimatorListenerAdapter() { 426 @Override 427 public void onAnimationEnd(Animator animation) { 428 finishImmediatelyWithNoAnimation(); 429 } 430 }); 431 anim.start(); 432 } 433 }; 434 435 animatePhoto(mPhotoStartParams); 436 animateAwayBackground(); 437 } 438 439 private void animatePhoto(MarginLayoutParams to) { 440 // Cancel any existing animation. 441 if (mPhotoAnimator != null) { 442 mPhotoAnimator.cancel(); 443 } 444 445 mPhotoView.setLayoutParams(to); 446 mAnimationPending = true; 447 mPhotoView.requestLayout(); 448 } 449 450 private void animateInBackground() { 451 ObjectAnimator.ofFloat(mBackdrop, "alpha", 0, 0.5f).setDuration( 452 PHOTO_EXPAND_DURATION).start(); 453 } 454 455 private void animateAwayBackground() { 456 ObjectAnimator.ofFloat(mBackdrop, "alpha", 0f).setDuration( 457 BACKDROP_FADEOUT_DURATION).start(); 458 } 459 460 @Override 461 protected void onSaveInstanceState(Bundle outState) { 462 super.onSaveInstanceState(outState); 463 outState.putParcelable(KEY_CURRENT_PHOTO_URI, mCurrentPhotoUri); 464 outState.putBoolean(KEY_SUB_ACTIVITY_IN_PROGRESS, mSubActivityInProgress); 465 } 466 467 @Override 468 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 469 if (mPhotoHandler != null) { 470 mSubActivityInProgress = false; 471 if (mPhotoHandler.handlePhotoActivityResult(requestCode, resultCode, data)) { 472 // Clear out any pending photo result. 473 mPendingPhotoResult = null; 474 } else { 475 // User cancelled the sub-activity and returning to the photo selection activity. 476 if (mCloseActivityWhenCameBackFromSubActivity) { 477 finishImmediatelyWithNoAnimation(); 478 } else { 479 // Re-display options. 480 mPhotoHandler.onClick(mPhotoView); 481 } 482 } 483 } else { 484 // Create a pending photo result to be handled when the photo handler is created. 485 mPendingPhotoResult = new PendingPhotoResult(requestCode, resultCode, data); 486 } 487 } 488 489 private void attachPhotoHandler() { 490 // Always provide the same two choices (take a photo with the camera, select a photo 491 // from the gallery), but with slightly different wording. 492 // Note: don't worry about this being a read-only contact; this code will not be invoked. 493 int mode = (mPhotoUri == null) ? PhotoActionPopup.Modes.NO_PHOTO 494 : PhotoActionPopup.Modes.PHOTO_DISALLOW_PRIMARY; 495 // We don't want to provide a choice to remove the photo for two reasons: 496 // 1) the UX designs don't call for it 497 // 2) even if we wanted to, the implementation would be moderately hairy 498 mode &= ~PhotoActionPopup.Flags.REMOVE_PHOTO; 499 500 mPhotoHandler = new PhotoHandler(this, mPhotoView, mode, mState); 501 502 if (mPendingPhotoResult != null) { 503 mPhotoHandler.handlePhotoActivityResult(mPendingPhotoResult.mRequestCode, 504 mPendingPhotoResult.mResultCode, mPendingPhotoResult.mData); 505 mPendingPhotoResult = null; 506 } else { 507 // Setting the photo in displayPhoto() resulted in a relayout 508 // request... to avoid jank, wait until this layout has happened. 509 SchedulingUtils.doAfterLayout(mBackdrop, new Runnable() { 510 @Override 511 public void run() { 512 animatePhotoOpen(); 513 } 514 }); 515 } 516 } 517 518 private final class PhotoHandler extends PhotoSelectionHandler { 519 private final PhotoActionListener mListener; 520 521 private PhotoHandler( 522 Context context, View photoView, int photoMode, RawContactDeltaList state) { 523 super(context, photoView, photoMode, PhotoSelectionActivity.this.mIsDirectoryContact, 524 state); 525 mListener = new PhotoListener(); 526 } 527 528 @Override 529 public PhotoActionListener getListener() { 530 return mListener; 531 } 532 533 @Override 534 public void startPhotoActivity(Intent intent, int requestCode, Uri photoUri) { 535 mSubActivityInProgress = true; 536 mCurrentPhotoUri = photoUri; 537 PhotoSelectionActivity.this.startActivityForResult(intent, requestCode); 538 } 539 540 private final class PhotoListener extends PhotoActionListener { 541 @Override 542 public void onPhotoSelected(Uri uri) { 543 RawContactDeltaList delta = getDeltaForAttachingPhotoToContact(); 544 long rawContactId = getWritableEntityId(); 545 546 Intent intent = ContactSaveService.createSaveContactIntent( 547 mContext, delta, "", 0, mIsProfile, null, null, rawContactId, uri); 548 startService(intent); 549 finish(); 550 } 551 552 @Override 553 public Uri getCurrentPhotoUri() { 554 return mCurrentPhotoUri; 555 } 556 557 @Override 558 public void onPhotoSelectionDismissed() { 559 if (!mSubActivityInProgress) { 560 finish(); 561 } 562 } 563 } 564 } 565 566 private static class PendingPhotoResult { 567 final private int mRequestCode; 568 final private int mResultCode; 569 final private Intent mData; 570 private PendingPhotoResult(int requestCode, int resultCode, Intent data) { 571 mRequestCode = requestCode; 572 mResultCode = resultCode; 573 mData = data; 574 } 575 } 576 } 577