/* * 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.common.model.RawContactDeltaList; 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 uri. */ private static final String KEY_CURRENT_PHOTO_URI = "currentphotouri"; /** 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 uri being interacted with, if any. Saved/restored between activity instances. */ private Uri mCurrentPhotoUri; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.photoselection_activity); if (savedInstanceState != null) { mCurrentPhotoUri = savedInstanceState.getParcelable(KEY_CURRENT_PHOTO_URI); 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 /* darkTheme */, false /* isCircular */, null); } else { // If we don't have a URI, just display an empty ImageView. The default image from the // ContactDetailFragment will show up in the background instead. mPhotoView.setImageDrawable(null); } 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. *

* 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.putParcelable(KEY_CURRENT_PHOTO_URI, mCurrentPhotoUri); 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, Uri photoUri) { mSubActivityInProgress = true; mCurrentPhotoUri = photoUri; PhotoSelectionActivity.this.startActivityForResult(intent, requestCode); } private final class PhotoListener extends PhotoActionListener { @Override public void onPhotoSelected(Uri uri) { RawContactDeltaList delta = getDeltaForAttachingPhotoToContact(); long rawContactId = getWritableEntityId(); Intent intent = ContactSaveService.createSaveContactIntent( mContext, delta, "", 0, mIsProfile, null, null, rawContactId, uri); startService(intent); finish(); } @Override public Uri getCurrentPhotoUri() { return mCurrentPhotoUri; } @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; } } }