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