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