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 android.animation.Animator;
19import android.animation.AnimatorListenerAdapter;
20import android.animation.ObjectAnimator;
21import android.animation.PropertyValuesHolder;
22import android.app.Activity;
23import android.content.Context;
24import android.content.Intent;
25import android.content.res.Configuration;
26import android.graphics.Bitmap;
27import android.graphics.Rect;
28import android.net.Uri;
29import android.os.Bundle;
30import android.os.Parcelable;
31import android.support.v4.content.FileProvider;
32import android.view.View;
33import android.view.ViewGroup.MarginLayoutParams;
34import android.widget.FrameLayout.LayoutParams;
35import android.widget.ImageView;
36
37import com.android.contacts.common.ContactPhotoManager;
38import com.android.contacts.ContactSaveService;
39import com.android.contacts.R;
40import com.android.contacts.detail.PhotoSelectionHandler;
41import com.android.contacts.editor.PhotoActionPopup;
42import com.android.contacts.common.model.RawContactDeltaList;
43import com.android.contacts.util.ContactPhotoUtils;
44import com.android.contacts.util.SchedulingUtils;
45
46import java.io.File;
47import java.io.FileNotFoundException;
48
49
50/**
51 * Popup activity for choosing a contact photo within the Contacts app.
52 */
53public 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