• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
onCreate(Bundle savedInstanceState)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      */
getAdjustedExpandedPhotoSize(View enclosingView, int heightOffset)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
onConfigurationChanged(Configuration newConfig)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
finish()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      */
buildIntent(Context context, Uri photoUri, Bitmap photoBitmap, byte[] photoBytes, Rect photoBounds, RawContactDeltaList delta, boolean isProfile, boolean isDirectoryContact, boolean expandPhotoOnClick)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 
finishImmediatelyWithNoAnimation()284     private void finishImmediatelyWithNoAnimation() {
285         super.finish();
286     }
287 
288     @Override
onDestroy()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 
displayPhoto()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      */
getPhotoEndParams()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 
animatePhotoOpen()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 
closePhotoAndFinish()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 
animatePhoto(MarginLayoutParams to)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 
animateInBackground()445     private void animateInBackground() {
446         ObjectAnimator.ofFloat(mBackdrop, "alpha", 0, 0.5f).setDuration(
447                 PHOTO_EXPAND_DURATION).start();
448     }
449 
animateAwayBackground()450     private void animateAwayBackground() {
451         ObjectAnimator.ofFloat(mBackdrop, "alpha", 0f).setDuration(
452                 BACKDROP_FADEOUT_DURATION).start();
453     }
454 
455     @Override
onSaveInstanceState(Bundle outState)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
onActivityResult(int requestCode, int resultCode, Intent data)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 
attachPhotoHandler()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 
PhotoHandler( Context context, View photoView, int photoMode, RawContactDeltaList state)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
getListener()524         public PhotoActionListener getListener() {
525             return mListener;
526         }
527 
528         @Override
startPhotoActivity(Intent intent, int requestCode, Uri photoUri)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
onPhotoSelected(Uri uri)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
getCurrentPhotoUri()548             public Uri getCurrentPhotoUri() {
549                 return mCurrentPhotoUri;
550             }
551 
552             @Override
onPhotoSelectionDismissed()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;
PendingPhotoResult(int requestCode, int resultCode, Intent data)565         private PendingPhotoResult(int requestCode, int resultCode, Intent data) {
566             mRequestCode = requestCode;
567             mResultCode = resultCode;
568             mData = data;
569         }
570     }
571 }
572