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