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