blob: da0d2e4a5bec2e015144eb83b1898211d3e4b328 [file] [log] [blame]
/*
* 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.
* <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.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;
}
}
}