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