1/*
2 * Copyright (C) 2015 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
17package com.android.messaging.ui;
18
19import android.animation.Animator;
20import android.animation.ObjectAnimator;
21import android.content.Context;
22import android.graphics.Rect;
23import android.os.Handler;
24import android.os.Looper;
25import android.util.AttributeSet;
26import android.view.LayoutInflater;
27import android.view.View;
28import android.widget.FrameLayout;
29import android.widget.ImageButton;
30import android.widget.ScrollView;
31
32import com.android.messaging.R;
33import com.android.messaging.annotation.VisibleForAnimation;
34import com.android.messaging.datamodel.data.DraftMessageData;
35import com.android.messaging.datamodel.data.MediaPickerMessagePartData;
36import com.android.messaging.datamodel.data.MessagePartData;
37import com.android.messaging.datamodel.data.PendingAttachmentData;
38import com.android.messaging.ui.MultiAttachmentLayout.OnAttachmentClickListener;
39import com.android.messaging.ui.animation.PopupTransitionAnimation;
40import com.android.messaging.ui.conversation.ComposeMessageView;
41import com.android.messaging.ui.conversation.ConversationFragment;
42import com.android.messaging.util.Assert;
43import com.android.messaging.util.ThreadUtil;
44import com.android.messaging.util.UiUtils;
45
46import java.util.ArrayList;
47import java.util.List;
48
49public class AttachmentPreview extends ScrollView implements OnAttachmentClickListener {
50    private FrameLayout mAttachmentView;
51    private ComposeMessageView mComposeMessageView;
52    private ImageButton mCloseButton;
53    private int mAnimatedHeight = -1;
54    private Animator mCloseGapAnimator;
55    private boolean mPendingFirstUpdate;
56    private Handler mHandler;
57    private Runnable mHideRunnable;
58    private boolean mPendingHideCanceled;
59
60    private static final int CLOSE_BUTTON_REVEAL_STAGGER_MILLIS = 300;
61
62    public AttachmentPreview(final Context context, final AttributeSet attrs) {
63        super(context, attrs);
64        mHandler = new Handler(Looper.getMainLooper());
65    }
66
67    @Override
68    protected void onFinishInflate() {
69        super.onFinishInflate();
70        mCloseButton = (ImageButton) findViewById(R.id.close_button);
71        mCloseButton.setOnClickListener(new OnClickListener() {
72            @Override
73            public void onClick(final View view) {
74                mComposeMessageView.clearAttachments();
75            }
76        });
77
78        mAttachmentView = (FrameLayout) findViewById(R.id.attachment_view);
79
80        // The attachment preview is a scroll view so that it can show the bottom portion of the
81        // attachment whenever the space is tight (e.g. when in landscape mode). Per design
82        // request we'd like to make the attachment view always scrolled to the bottom.
83        addOnLayoutChangeListener(new OnLayoutChangeListener() {
84            @Override
85            public void onLayoutChange(final View v, final int left, final int top, final int right,
86                    final int bottom, final int oldLeft, final int oldTop, final int oldRight,
87                    final int oldBottom) {
88                post(new Runnable() {
89                    @Override
90                    public void run() {
91                        final int childCount = getChildCount();
92                        if (childCount > 0) {
93                            final View lastChild = getChildAt(childCount - 1);
94                            scrollTo(getScrollX(), lastChild.getBottom() - getHeight());
95                        }
96                    }
97                });
98            }
99        });
100        mPendingFirstUpdate = true;
101    }
102
103    public void setComposeMessageView(final ComposeMessageView composeMessageView) {
104        mComposeMessageView = composeMessageView;
105    }
106
107    @Override
108    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
109        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
110        if (mAnimatedHeight >= 0) {
111            setMeasuredDimension(getMeasuredWidth(), mAnimatedHeight);
112        }
113    }
114
115    private void cancelPendingHide() {
116        mPendingHideCanceled = true;
117    }
118
119    public void hideAttachmentPreview() {
120        if (getVisibility() != GONE) {
121            UiUtils.revealOrHideViewWithAnimation(mCloseButton, GONE,
122                    null /* onFinishRunnable */);
123            startCloseGapAnimationOnAttachmentClear();
124
125            if (mAttachmentView.getChildCount() > 0) {
126                mPendingHideCanceled = false;
127                final View viewToHide = mAttachmentView.getChildCount() > 1 ?
128                        mAttachmentView : mAttachmentView.getChildAt(0);
129                UiUtils.revealOrHideViewWithAnimation(viewToHide, INVISIBLE,
130                        new Runnable() {
131                            @Override
132                            public void run() {
133                                // Only hide if we are didn't get overruled by showing
134                                if (!mPendingHideCanceled) {
135                                    mAttachmentView.removeAllViews();
136                                    setVisibility(GONE);
137                                }
138                            }
139                        });
140            } else {
141                mAttachmentView.removeAllViews();
142                setVisibility(GONE);
143            }
144        }
145    }
146
147    // returns true if we have attachments
148    public boolean onAttachmentsChanged(final DraftMessageData draftMessageData) {
149        final boolean isFirstUpdate = mPendingFirstUpdate;
150        final List<MessagePartData> attachments = draftMessageData.getReadOnlyAttachments();
151        final List<PendingAttachmentData> pendingAttachments =
152                draftMessageData.getReadOnlyPendingAttachments();
153
154        // Any change in attachments would invalidate the animated height animation.
155        cancelCloseGapAnimation();
156        mPendingFirstUpdate = false;
157
158        final int combinedAttachmentCount = attachments.size() + pendingAttachments.size();
159        mCloseButton.setContentDescription(getResources()
160                .getQuantityString(R.plurals.attachment_preview_close_content_description,
161                        combinedAttachmentCount));
162        if (combinedAttachmentCount == 0) {
163            mHideRunnable = new Runnable() {
164                @Override
165                public void run() {
166                    mHideRunnable = null;
167                    // Only start the hiding if there are still no attachments
168                    if (attachments.size() + pendingAttachments.size() == 0) {
169                        hideAttachmentPreview();
170                    }
171                }
172            };
173            if (draftMessageData.isSending()) {
174                // Wait to hide until the message is ready to start animating
175                // We'll execute immediately when the animation triggers
176                mHandler.postDelayed(mHideRunnable,
177                        ConversationFragment.MESSAGE_ANIMATION_MAX_WAIT);
178            } else {
179                // Run immediately when clearing attachments
180                mHideRunnable.run();
181            }
182            return false;
183        }
184
185        cancelPendingHide();  // We're showing
186        if (getVisibility() != VISIBLE) {
187            setVisibility(VISIBLE);
188            mAttachmentView.setVisibility(VISIBLE);
189
190            // Don't animate in the close button if this is the first update after view creation.
191            // This is the initial draft load from database for pre-existing drafts.
192            if (!isFirstUpdate) {
193                // Reveal the close button after the view animates in.
194                mCloseButton.setVisibility(INVISIBLE);
195                ThreadUtil.getMainThreadHandler().postDelayed(new Runnable() {
196                    @Override
197                    public void run() {
198                        UiUtils.revealOrHideViewWithAnimation(mCloseButton, VISIBLE,
199                                null /* onFinishRunnable */);
200                    }
201                }, UiUtils.MEDIAPICKER_TRANSITION_DURATION + CLOSE_BUTTON_REVEAL_STAGGER_MILLIS);
202            }
203        }
204
205        // Merge the pending attachment list with real attachment.  Design would prefer these be
206        // in LIFO order user can see added images past the 5th one but we also want them to be in
207        // order and we want it to be WYSIWYG.
208        final List<MessagePartData> combinedAttachments = new ArrayList<>();
209        combinedAttachments.addAll(attachments);
210        combinedAttachments.addAll(pendingAttachments);
211
212        final LayoutInflater layoutInflater = LayoutInflater.from(getContext());
213        if (combinedAttachmentCount > 1) {
214            MultiAttachmentLayout multiAttachmentLayout = null;
215            Rect transitionRect = null;
216            if (mAttachmentView.getChildCount() > 0) {
217                final View firstChild = mAttachmentView.getChildAt(0);
218                if (firstChild instanceof MultiAttachmentLayout) {
219                    Assert.equals(1, mAttachmentView.getChildCount());
220                    multiAttachmentLayout = (MultiAttachmentLayout) firstChild;
221                    multiAttachmentLayout.bindAttachments(combinedAttachments,
222                            null /* transitionRect */, combinedAttachmentCount);
223                } else {
224                    transitionRect = new Rect(firstChild.getLeft(), firstChild.getTop(),
225                            firstChild.getRight(), firstChild.getBottom());
226                }
227            }
228            if (multiAttachmentLayout == null) {
229                multiAttachmentLayout = AttachmentPreviewFactory.createMultiplePreview(
230                        getContext(), this);
231                multiAttachmentLayout.bindAttachments(combinedAttachments, transitionRect,
232                        combinedAttachmentCount);
233                mAttachmentView.removeAllViews();
234                mAttachmentView.addView(multiAttachmentLayout);
235            }
236        } else {
237            final MessagePartData attachment = combinedAttachments.get(0);
238            boolean shouldAnimate = true;
239            if (mAttachmentView.getChildCount() > 0) {
240                // If we are going from N->1 attachments, try to use the current bounds
241                // bounds as the starting rect.
242                shouldAnimate = false;
243                final View firstChild = mAttachmentView.getChildAt(0);
244                if (firstChild instanceof MultiAttachmentLayout &&
245                        attachment instanceof MediaPickerMessagePartData) {
246                    final View leftoverView = ((MultiAttachmentLayout) firstChild)
247                            .findViewForAttachment(attachment);
248                    if (leftoverView != null) {
249                        final Rect currentRect = UiUtils.getMeasuredBoundsOnScreen(leftoverView);
250                        if (!currentRect.isEmpty() &&
251                                attachment instanceof MediaPickerMessagePartData) {
252                            ((MediaPickerMessagePartData) attachment).setStartRect(currentRect);
253                            shouldAnimate = true;
254                        }
255                    }
256                }
257            }
258            mAttachmentView.removeAllViews();
259            final View attachmentView = AttachmentPreviewFactory.createAttachmentPreview(
260                    layoutInflater, attachment, mAttachmentView,
261                    AttachmentPreviewFactory.TYPE_SINGLE, true /* startImageRequest */, this);
262            if (attachmentView != null) {
263                mAttachmentView.addView(attachmentView);
264                if (shouldAnimate) {
265                    tryAnimateViewIn(attachment, attachmentView);
266                }
267            }
268        }
269        return true;
270    }
271
272    public void onMessageAnimationStart() {
273        if (mHideRunnable == null) {
274            return;
275        }
276
277        // Run the hide animation at the same time as the message animation
278        mHandler.removeCallbacks(mHideRunnable);
279        setVisibility(View.INVISIBLE);
280        mHideRunnable.run();
281    }
282
283    static void tryAnimateViewIn(final MessagePartData attachmentData, final View view) {
284        if (attachmentData instanceof MediaPickerMessagePartData) {
285            final Rect startRect = ((MediaPickerMessagePartData) attachmentData).getStartRect();
286            new PopupTransitionAnimation(startRect, view).startAfterLayoutComplete();
287        }
288    }
289
290    @VisibleForAnimation
291    public void setAnimatedHeight(final int animatedHeight) {
292        if (mAnimatedHeight != animatedHeight) {
293            mAnimatedHeight = animatedHeight;
294            requestLayout();
295        }
296    }
297
298    /**
299     * Kicks off an animation to animate the layout change for closing the gap between the
300     * message list and the compose message box when the attachments are cleared.
301     */
302    private void startCloseGapAnimationOnAttachmentClear() {
303        // Cancel existing animation.
304        cancelCloseGapAnimation();
305        mCloseGapAnimator = ObjectAnimator.ofInt(this, "animatedHeight", getHeight(), 0);
306        mCloseGapAnimator.start();
307    }
308
309    private void cancelCloseGapAnimation() {
310        if (mCloseGapAnimator != null) {
311            mCloseGapAnimator.cancel();
312            mCloseGapAnimator = null;
313        }
314        mAnimatedHeight = -1;
315    }
316
317    @Override
318    public boolean onAttachmentClick(final MessagePartData attachment,
319            final Rect viewBoundsOnScreen, final boolean longPress) {
320        if (longPress) {
321            mComposeMessageView.onAttachmentPreviewLongClicked();
322            return true;
323        }
324
325        if (!(attachment instanceof PendingAttachmentData) && attachment.isImage()) {
326            mComposeMessageView.displayPhoto(attachment.getContentUri(), viewBoundsOnScreen);
327            return true;
328        }
329        return false;
330    }
331}
332