| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.contacts.activities; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.ObjectAnimator; |
| import android.animation.PropertyValuesHolder; |
| import android.app.Activity; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.res.Configuration; |
| import android.graphics.Bitmap; |
| import android.graphics.Rect; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.Parcelable; |
| import android.view.View; |
| import android.view.ViewGroup.MarginLayoutParams; |
| import android.widget.FrameLayout.LayoutParams; |
| import android.widget.ImageView; |
| |
| import com.android.contacts.common.ContactPhotoManager; |
| import com.android.contacts.ContactSaveService; |
| import com.android.contacts.R; |
| import com.android.contacts.detail.PhotoSelectionHandler; |
| import com.android.contacts.editor.PhotoActionPopup; |
| import com.android.contacts.model.RawContactDeltaList; |
| import com.android.contacts.util.ContactPhotoUtils; |
| import com.android.contacts.util.SchedulingUtils; |
| |
| |
| /** |
| * Popup activity for choosing a contact photo within the Contacts app. |
| */ |
| public class PhotoSelectionActivity extends Activity { |
| |
| private static final String TAG = "PhotoSelectionActivity"; |
| |
| /** Number of ms for the animation to expand the photo. */ |
| private static final int PHOTO_EXPAND_DURATION = 100; |
| |
| /** Number of ms for the animation to contract the photo on activity exit. */ |
| private static final int PHOTO_CONTRACT_DURATION = 50; |
| |
| /** Number of ms for the animation to hide the backdrop on finish. */ |
| private static final int BACKDROP_FADEOUT_DURATION = 100; |
| |
| /** Key used to persist photo-filename (NOT full file-path). */ |
| private static final String KEY_CURRENT_PHOTO_FILE = "currentphotofile"; |
| |
| /** Key used to persist whether a sub-activity is currently in progress. */ |
| private static final String KEY_SUB_ACTIVITY_IN_PROGRESS = "subinprogress"; |
| |
| /** Intent extra to get the photo URI. */ |
| public static final String PHOTO_URI = "photo_uri"; |
| |
| /** Intent extra to get the entity delta list. */ |
| public static final String ENTITY_DELTA_LIST = "entity_delta_list"; |
| |
| /** Intent extra to indicate whether the contact is the user's profile. */ |
| public static final String IS_PROFILE = "is_profile"; |
| |
| /** Intent extra to indicate whether the contact is from a directory (non-editable). */ |
| public static final String IS_DIRECTORY_CONTACT = "is_directory_contact"; |
| |
| /** |
| * Intent extra to indicate whether the photo should be animated to show the full contents of |
| * the photo (on a larger portion of the screen) when clicked. If unspecified or false, the |
| * photo will not move from its original location. |
| */ |
| public static final String EXPAND_PHOTO = "expand_photo"; |
| |
| /** Source bounds of the image that was clicked on. */ |
| private Rect mSourceBounds; |
| |
| /** |
| * The photo URI. May be null, in which case the default avatar will be used. |
| */ |
| private Uri mPhotoUri; |
| |
| /** Entity delta list of the contact. */ |
| private RawContactDeltaList mState; |
| |
| /** Whether the contact is the user's profile. */ |
| private boolean mIsProfile; |
| |
| /** Whether the contact is from a directory. */ |
| private boolean mIsDirectoryContact; |
| |
| /** Whether to animate the photo to an expanded view covering more of the screen. */ |
| private boolean mExpandPhoto; |
| |
| /** |
| * Side length (in pixels) of the expanded photo if to be expanded. Photos are expected to |
| * be square. |
| */ |
| private int mExpandedPhotoSize; |
| |
| /** Height (in pixels) to leave underneath the expanded photo to show the list popup */ |
| private int mHeightOffset; |
| |
| /** The semi-transparent backdrop. */ |
| private View mBackdrop; |
| |
| /** The photo view. */ |
| private ImageView mPhotoView; |
| |
| /** The photo handler attached to this activity, if any. */ |
| private PhotoHandler mPhotoHandler; |
| |
| /** Animator to expand the photo out to full size. */ |
| private ObjectAnimator mPhotoAnimator; |
| |
| /** Listener for the animation. */ |
| private AnimatorListenerAdapter mAnimationListener; |
| |
| /** Whether a change in layout of the photo has occurred that has no animation yet. */ |
| private boolean mAnimationPending; |
| |
| /** Prior position of the image (for animating). */ |
| Rect mOriginalPos = new Rect(); |
| |
| /** Layout params for the photo view before we started animating. */ |
| private LayoutParams mPhotoStartParams; |
| |
| /** Layout params for the photo view after we finished animating. */ |
| private LayoutParams mPhotoEndParams; |
| |
| /** Whether a sub-activity is currently in progress. */ |
| private boolean mSubActivityInProgress; |
| |
| private boolean mCloseActivityWhenCameBackFromSubActivity; |
| |
| /** |
| * A photo result received by the activity, persisted across activity lifecycle. |
| */ |
| private PendingPhotoResult mPendingPhotoResult; |
| |
| /** |
| * The photo file being interacted with, if any. Saved/restored between activity instances. |
| */ |
| private String mCurrentPhotoFile; |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| setContentView(R.layout.photoselection_activity); |
| if (savedInstanceState != null) { |
| mCurrentPhotoFile = savedInstanceState.getString(KEY_CURRENT_PHOTO_FILE); |
| mSubActivityInProgress = savedInstanceState.getBoolean(KEY_SUB_ACTIVITY_IN_PROGRESS); |
| } |
| |
| // Pull data out of the intent. |
| final Intent intent = getIntent(); |
| mPhotoUri = intent.getParcelableExtra(PHOTO_URI); |
| mState = (RawContactDeltaList) intent.getParcelableExtra(ENTITY_DELTA_LIST); |
| mIsProfile = intent.getBooleanExtra(IS_PROFILE, false); |
| mIsDirectoryContact = intent.getBooleanExtra(IS_DIRECTORY_CONTACT, false); |
| mExpandPhoto = intent.getBooleanExtra(EXPAND_PHOTO, false); |
| |
| // Pull out photo expansion properties from resources |
| mExpandedPhotoSize = getResources().getDimensionPixelSize( |
| R.dimen.detail_contact_photo_expanded_size); |
| mHeightOffset = getResources().getDimensionPixelOffset( |
| R.dimen.expanded_photo_height_offset); |
| |
| mBackdrop = findViewById(R.id.backdrop); |
| mPhotoView = (ImageView) findViewById(R.id.photo); |
| mSourceBounds = intent.getSourceBounds(); |
| |
| // Fade in the background. |
| animateInBackground(); |
| |
| // Dismiss the dialog on clicking the backdrop. |
| mBackdrop.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| finish(); |
| } |
| }); |
| |
| // Wait until the layout pass to show the photo, so that the source bounds will match up. |
| SchedulingUtils.doAfterLayout(mBackdrop, new Runnable() { |
| @Override |
| public void run() { |
| displayPhoto(); |
| } |
| }); |
| } |
| |
| /** |
| * Compute the adjusted expanded photo size to fit within the enclosing view with the same |
| * aspect ratio. |
| * @param enclosingView This is the view that the photo must fit within. |
| * @param heightOffset This is the amount of height to leave open for the photo action popup. |
| */ |
| private int getAdjustedExpandedPhotoSize(View enclosingView, int heightOffset) { |
| // pull out the bounds of the backdrop |
| final Rect bounds = new Rect(); |
| enclosingView.getDrawingRect(bounds); |
| final int boundsWidth = bounds.width(); |
| final int boundsHeight = bounds.height() - heightOffset; |
| |
| // ensure that the new expanded photo size can fit within the backdrop |
| final float alpha = Math.min((float) boundsHeight / (float) mExpandedPhotoSize, |
| (float) boundsWidth / (float) mExpandedPhotoSize); |
| if (alpha < 1.0f) { |
| // need to shrink width and height while maintaining aspect ratio |
| return (int) (alpha * mExpandedPhotoSize); |
| } else { |
| return mExpandedPhotoSize; |
| } |
| } |
| |
| @Override |
| public void onConfigurationChanged(Configuration newConfig) { |
| super.onConfigurationChanged(newConfig); |
| |
| // The current look may not seem right on the new configuration, so let's just close self. |
| |
| if (!mSubActivityInProgress) { |
| finishImmediatelyWithNoAnimation(); |
| } else { |
| // A sub-activity is in progress, so don't close it yet, but close it when we come back |
| // to this activity. |
| mCloseActivityWhenCameBackFromSubActivity = true; |
| } |
| } |
| |
| @Override |
| public void finish() { |
| if (!mSubActivityInProgress) { |
| closePhotoAndFinish(); |
| } else { |
| finishImmediatelyWithNoAnimation(); |
| } |
| } |
| |
| /** |
| * Builds a well-formed intent for invoking this activity. |
| * @param context The context. |
| * @param photoUri The URI of the current photo (may be null, in which case the default |
| * avatar image will be displayed). |
| * @param photoBitmap The bitmap of the current photo (may be null, in which case the default |
| * avatar image will be displayed). |
| * @param photoBytes The bytes for the current photo (may be null, in which case the default |
| * avatar image will be displayed). |
| * @param photoBounds The pixel bounds of the current photo. |
| * @param delta The entity delta list for the contact. |
| * @param isProfile Whether the contact is the user's profile. |
| * @param isDirectoryContact Whether the contact comes from a directory (non-editable). |
| * @param expandPhotoOnClick Whether the photo should be expanded on click or not (generally, |
| * this should be true for phones, and false for tablets). |
| * @return An intent that can be used to invoke the photo selection activity. |
| */ |
| public static Intent buildIntent(Context context, Uri photoUri, Bitmap photoBitmap, |
| byte[] photoBytes, Rect photoBounds, RawContactDeltaList delta, boolean isProfile, |
| boolean isDirectoryContact, boolean expandPhotoOnClick) { |
| Intent intent = new Intent(context, PhotoSelectionActivity.class); |
| if (photoUri != null && photoBitmap != null && photoBytes != null) { |
| intent.putExtra(PHOTO_URI, photoUri); |
| } |
| intent.setSourceBounds(photoBounds); |
| intent.putExtra(ENTITY_DELTA_LIST, (Parcelable) delta); |
| intent.putExtra(IS_PROFILE, isProfile); |
| intent.putExtra(IS_DIRECTORY_CONTACT, isDirectoryContact); |
| intent.putExtra(EXPAND_PHOTO, expandPhotoOnClick); |
| return intent; |
| } |
| |
| private void finishImmediatelyWithNoAnimation() { |
| super.finish(); |
| } |
| |
| @Override |
| protected void onDestroy() { |
| super.onDestroy(); |
| if (mPhotoAnimator != null) { |
| mPhotoAnimator.cancel(); |
| mPhotoAnimator = null; |
| } |
| if (mPhotoHandler != null) { |
| mPhotoHandler.destroy(); |
| mPhotoHandler = null; |
| } |
| } |
| |
| private void displayPhoto() { |
| // Animate the photo view into its end location. |
| final int[] pos = new int[2]; |
| mBackdrop.getLocationOnScreen(pos); |
| LayoutParams layoutParams = new LayoutParams(mSourceBounds.width(), |
| mSourceBounds.height()); |
| mOriginalPos.left = mSourceBounds.left - pos[0]; |
| mOriginalPos.top = mSourceBounds.top - pos[1]; |
| mOriginalPos.right = mOriginalPos.left + mSourceBounds.width(); |
| mOriginalPos.bottom = mOriginalPos.top + mSourceBounds.height(); |
| layoutParams.setMargins(mOriginalPos.left, mOriginalPos.top, mOriginalPos.right, |
| mOriginalPos.bottom); |
| mPhotoStartParams = layoutParams; |
| mPhotoView.setLayoutParams(layoutParams); |
| mPhotoView.requestLayout(); |
| |
| // Load the photo. |
| int photoWidth = getPhotoEndParams().width; |
| if (mPhotoUri != null) { |
| // If we have a URI, the bitmap should be cached directly. |
| ContactPhotoManager.getInstance(this).loadPhoto(mPhotoView, mPhotoUri, photoWidth, |
| false); |
| } else { |
| // Fall back to avatar image. |
| mPhotoView.setImageResource(ContactPhotoManager.getDefaultAvatarResId(this, photoWidth, |
| false)); |
| } |
| |
| mPhotoView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { |
| @Override |
| public void onLayoutChange(View v, int left, int top, int right, int bottom, |
| int oldLeft, int oldTop, int oldRight, int oldBottom) { |
| if (mAnimationPending) { |
| mAnimationPending = false; |
| PropertyValuesHolder pvhLeft = |
| PropertyValuesHolder.ofInt("left", mOriginalPos.left, left); |
| PropertyValuesHolder pvhTop = |
| PropertyValuesHolder.ofInt("top", mOriginalPos.top, top); |
| PropertyValuesHolder pvhRight = |
| PropertyValuesHolder.ofInt("right", mOriginalPos.right, right); |
| PropertyValuesHolder pvhBottom = |
| PropertyValuesHolder.ofInt("bottom", mOriginalPos.bottom, bottom); |
| ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(mPhotoView, |
| pvhLeft, pvhTop, pvhRight, pvhBottom).setDuration( |
| PHOTO_EXPAND_DURATION); |
| if (mAnimationListener != null) { |
| anim.addListener(mAnimationListener); |
| } |
| anim.start(); |
| } |
| } |
| }); |
| attachPhotoHandler(); |
| } |
| |
| /** |
| * This sets the photo's layout params at the end of the animation. |
| * <p> |
| * The scheme is to enlarge the photo to the desired size with the enlarged photo shifted |
| * to the top left of the screen as much as possible while keeping the underlying smaller |
| * photo occluded. |
| */ |
| private LayoutParams getPhotoEndParams() { |
| if (mPhotoEndParams == null) { |
| mPhotoEndParams = new LayoutParams(mPhotoStartParams); |
| if (mExpandPhoto) { |
| final int adjustedPhotoSize = getAdjustedExpandedPhotoSize(mBackdrop, |
| mHeightOffset); |
| int widthDelta = adjustedPhotoSize - mPhotoStartParams.width; |
| int heightDelta = adjustedPhotoSize - mPhotoStartParams.height; |
| if (widthDelta >= 1 || heightDelta >= 1) { |
| // This is an actual expansion. |
| mPhotoEndParams.width = adjustedPhotoSize; |
| mPhotoEndParams.height = adjustedPhotoSize; |
| mPhotoEndParams.topMargin = |
| Math.max(mPhotoStartParams.topMargin - heightDelta, 0); |
| mPhotoEndParams.leftMargin = |
| Math.max(mPhotoStartParams.leftMargin - widthDelta, 0); |
| mPhotoEndParams.bottomMargin = 0; |
| mPhotoEndParams.rightMargin = 0; |
| } |
| } |
| } |
| return mPhotoEndParams; |
| } |
| |
| private void animatePhotoOpen() { |
| mAnimationListener = new AnimatorListenerAdapter() { |
| private void capturePhotoPos() { |
| mPhotoView.requestLayout(); |
| mOriginalPos.left = mPhotoView.getLeft(); |
| mOriginalPos.top = mPhotoView.getTop(); |
| mOriginalPos.right = mPhotoView.getRight(); |
| mOriginalPos.bottom = mPhotoView.getBottom(); |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| capturePhotoPos(); |
| if (mPhotoHandler != null) { |
| mPhotoHandler.onClick(mPhotoView); |
| } |
| } |
| |
| @Override |
| public void onAnimationCancel(Animator animation) { |
| capturePhotoPos(); |
| } |
| }; |
| animatePhoto(getPhotoEndParams()); |
| } |
| |
| private void closePhotoAndFinish() { |
| mAnimationListener = new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| // After the photo animates down, fade it away and finish. |
| ObjectAnimator anim = ObjectAnimator.ofFloat( |
| mPhotoView, "alpha", 0f).setDuration(PHOTO_CONTRACT_DURATION); |
| anim.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| finishImmediatelyWithNoAnimation(); |
| } |
| }); |
| anim.start(); |
| } |
| }; |
| |
| animatePhoto(mPhotoStartParams); |
| animateAwayBackground(); |
| } |
| |
| private void animatePhoto(MarginLayoutParams to) { |
| // Cancel any existing animation. |
| if (mPhotoAnimator != null) { |
| mPhotoAnimator.cancel(); |
| } |
| |
| mPhotoView.setLayoutParams(to); |
| mAnimationPending = true; |
| mPhotoView.requestLayout(); |
| } |
| |
| private void animateInBackground() { |
| ObjectAnimator.ofFloat(mBackdrop, "alpha", 0, 0.5f).setDuration( |
| PHOTO_EXPAND_DURATION).start(); |
| } |
| |
| private void animateAwayBackground() { |
| ObjectAnimator.ofFloat(mBackdrop, "alpha", 0f).setDuration( |
| BACKDROP_FADEOUT_DURATION).start(); |
| } |
| |
| @Override |
| protected void onSaveInstanceState(Bundle outState) { |
| super.onSaveInstanceState(outState); |
| outState.putString(KEY_CURRENT_PHOTO_FILE, mCurrentPhotoFile); |
| outState.putBoolean(KEY_SUB_ACTIVITY_IN_PROGRESS, mSubActivityInProgress); |
| } |
| |
| @Override |
| protected void onActivityResult(int requestCode, int resultCode, Intent data) { |
| if (mPhotoHandler != null) { |
| mSubActivityInProgress = false; |
| if (mPhotoHandler.handlePhotoActivityResult(requestCode, resultCode, data)) { |
| // Clear out any pending photo result. |
| mPendingPhotoResult = null; |
| } else { |
| // User cancelled the sub-activity and returning to the photo selection activity. |
| if (mCloseActivityWhenCameBackFromSubActivity) { |
| finishImmediatelyWithNoAnimation(); |
| } else { |
| // Re-display options. |
| mPhotoHandler.onClick(mPhotoView); |
| } |
| } |
| } else { |
| // Create a pending photo result to be handled when the photo handler is created. |
| mPendingPhotoResult = new PendingPhotoResult(requestCode, resultCode, data); |
| } |
| } |
| |
| private void attachPhotoHandler() { |
| // Always provide the same two choices (take a photo with the camera, select a photo |
| // from the gallery), but with slightly different wording. |
| // Note: don't worry about this being a read-only contact; this code will not be invoked. |
| int mode = (mPhotoUri == null) ? PhotoActionPopup.Modes.NO_PHOTO |
| : PhotoActionPopup.Modes.PHOTO_DISALLOW_PRIMARY; |
| // We don't want to provide a choice to remove the photo for two reasons: |
| // 1) the UX designs don't call for it |
| // 2) even if we wanted to, the implementation would be moderately hairy |
| mode &= ~PhotoActionPopup.Flags.REMOVE_PHOTO; |
| |
| mPhotoHandler = new PhotoHandler(this, mPhotoView, mode, mState); |
| |
| if (mPendingPhotoResult != null) { |
| mPhotoHandler.handlePhotoActivityResult(mPendingPhotoResult.mRequestCode, |
| mPendingPhotoResult.mResultCode, mPendingPhotoResult.mData); |
| mPendingPhotoResult = null; |
| } else { |
| // Setting the photo in displayPhoto() resulted in a relayout |
| // request... to avoid jank, wait until this layout has happened. |
| SchedulingUtils.doAfterLayout(mBackdrop, new Runnable() { |
| @Override |
| public void run() { |
| animatePhotoOpen(); |
| } |
| }); |
| } |
| } |
| |
| private final class PhotoHandler extends PhotoSelectionHandler { |
| private final PhotoActionListener mListener; |
| |
| private PhotoHandler( |
| Context context, View photoView, int photoMode, RawContactDeltaList state) { |
| super(context, photoView, photoMode, PhotoSelectionActivity.this.mIsDirectoryContact, |
| state); |
| mListener = new PhotoListener(); |
| } |
| |
| @Override |
| public PhotoActionListener getListener() { |
| return mListener; |
| } |
| |
| @Override |
| public void startPhotoActivity(Intent intent, int requestCode, String photoFile) { |
| mSubActivityInProgress = true; |
| mCurrentPhotoFile = photoFile; |
| PhotoSelectionActivity.this.startActivityForResult(intent, requestCode); |
| } |
| |
| private final class PhotoListener extends PhotoActionListener { |
| @Override |
| public void onPhotoSelected(Bitmap bitmap) { |
| RawContactDeltaList delta = getDeltaForAttachingPhotoToContact(); |
| long rawContactId = getWritableEntityId(); |
| final String croppedPath = ContactPhotoUtils.pathForCroppedPhoto( |
| PhotoSelectionActivity.this, mCurrentPhotoFile); |
| Intent intent = ContactSaveService.createSaveContactIntent( |
| mContext, delta, "", 0, mIsProfile, null, null, rawContactId, croppedPath); |
| startService(intent); |
| finish(); |
| } |
| |
| @Override |
| public String getCurrentPhotoFile() { |
| return mCurrentPhotoFile; |
| } |
| |
| @Override |
| public void onPhotoSelectionDismissed() { |
| if (!mSubActivityInProgress) { |
| finish(); |
| } |
| } |
| } |
| } |
| |
| private static class PendingPhotoResult { |
| final private int mRequestCode; |
| final private int mResultCode; |
| final private Intent mData; |
| private PendingPhotoResult(int requestCode, int resultCode, Intent data) { |
| mRequestCode = requestCode; |
| mResultCode = resultCode; |
| mData = data; |
| } |
| } |
| } |