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