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