1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.ui;
19
20import android.animation.Animator.AnimatorListener;
21import android.animation.ObjectAnimator;
22import android.content.Context;
23import android.content.res.Resources;
24import android.util.AttributeSet;
25import android.view.View;
26import android.view.View.OnClickListener;
27import android.view.animation.DecelerateInterpolator;
28import android.widget.FrameLayout;
29import android.widget.TextView;
30
31import com.android.mail.R;
32import com.android.mail.analytics.Analytics;
33import com.android.mail.browse.ConversationCursor;
34import com.android.mail.browse.ConversationItemView;
35import com.android.mail.providers.Account;
36import com.android.mail.providers.Conversation;
37import com.android.mail.providers.Folder;
38import com.android.mail.utils.Utils;
39import com.google.common.collect.ImmutableList;
40
41public class LeaveBehindItem extends FrameLayout implements OnClickListener, SwipeableItemView {
42
43    private ToastBarOperation mUndoOp;
44    private Account mAccount;
45    private AnimatedAdapter mAdapter;
46    private TextView mText;
47    private View mSwipeableContent;
48    public int position;
49    private Conversation mData;
50    private int mWidth;
51    /**
52     * The height of this view. Typically, this matches the height of the originating
53     * {@link ConversationItemView}.
54     */
55    private int mHeight;
56    private int mAnimatedHeight = -1;
57    private boolean mAnimating;
58    private boolean mFadingInText;
59    private boolean mInert = false;
60    private ObjectAnimator mFadeIn;
61
62    private static int sShrinkAnimationDuration = -1;
63    private static int sFadeInAnimationDuration = -1;
64    private static float sScrollSlop;
65    private static final float OPAQUE = 1.0f;
66    private static final float TRANSPARENT = 0.0f;
67
68    public LeaveBehindItem(Context context) {
69        this(context, null);
70    }
71
72    public LeaveBehindItem(Context context, AttributeSet attrs) {
73        this(context, attrs, -1);
74    }
75
76    public LeaveBehindItem(Context context, AttributeSet attrs, int defStyle) {
77        super(context, attrs, defStyle);
78        loadStatics(context);
79    }
80
81    private static void loadStatics(final Context context) {
82        if (sShrinkAnimationDuration == -1) {
83            Resources res = context.getResources();
84            sShrinkAnimationDuration = res.getInteger(R.integer.shrink_animation_duration);
85            sFadeInAnimationDuration = res.getInteger(R.integer.fade_in_animation_duration);
86            sScrollSlop = res.getInteger(R.integer.leaveBehindSwipeScrollSlop);
87        }
88    }
89
90    @Override
91    public void onClick(View v) {
92        final int id = v.getId();
93        if (id == R.id.swipeable_content) {
94            if (mAccount.undoUri != null && !mInert) {
95                // NOTE: We might want undo to return the messages affected,
96                // in which case the resulting cursor might be interesting...
97                // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate
98                // the set of commands to undo
99                mAdapter.setSwipeUndo(true);
100                mAdapter.clearLeaveBehind(getConversationId());
101                ConversationCursor cursor = mAdapter.getConversationCursor();
102                if (cursor != null) {
103                    cursor.undo(getContext(), mAccount.undoUri);
104                }
105            }
106        } else if (id == R.id.undo_descriptionview) {
107            // Essentially, makes sure that tapping description view doesn't highlight
108            // either the undo button icon or text.
109        }
110    }
111
112    public void bind(int pos, Account account, AnimatedAdapter adapter,
113            ToastBarOperation undoOp, Conversation target, Folder folder, int height) {
114        position = pos;
115        mUndoOp = undoOp;
116        mAccount = account;
117        mAdapter = adapter;
118        mHeight = height;
119        setData(target);
120        mSwipeableContent = findViewById(R.id.swipeable_content);
121        // Listen on swipeable content so that we can show both the undo icon
122        // and button text as selected since they set duplicateParentState to true
123        mSwipeableContent.setOnClickListener(this);
124        mSwipeableContent.setAlpha(TRANSPARENT);
125        mText = ((TextView) findViewById(R.id.undo_descriptionview));
126        mText.setText(Utils.convertHtmlToPlainText(mUndoOp
127                .getSingularDescription(getContext(), folder)));
128        mText.setOnClickListener(this);
129    }
130
131    public void commit() {
132        ConversationCursor cursor = mAdapter.getConversationCursor();
133        if (cursor != null) {
134            cursor.delete(ImmutableList.of(getData()));
135        }
136    }
137
138    @Override
139    public void dismiss() {
140        if (mAdapter != null) {
141            Analytics.getInstance().sendEvent("list_swipe", "leave_behind", null, 0);
142            mAdapter.fadeOutSpecificLeaveBehindItem(mData.id);
143            mAdapter.notifyDataSetChanged();
144        }
145    }
146
147    public long getConversationId() {
148        return getData().id;
149    }
150
151    @Override
152    public SwipeableView getSwipeableView() {
153        return SwipeableView.from(mSwipeableContent);
154    }
155
156    @Override
157    public boolean canChildBeDismissed() {
158        return !mInert;
159    }
160
161    public LeaveBehindData getLeaveBehindData() {
162        return new LeaveBehindData(getData(), mUndoOp, mHeight);
163    }
164
165    /**
166     * Animate shrinking the height of this view.
167     * @param item the conversation to animate
168     * @param listener the method to call when the animation is done
169     * @param undo true if an operation is being undone. We animate the item
170     *            away during delete. Undoing populates the item.
171     */
172    public void startShrinkAnimation(AnimatorListener listener) {
173        if (!mAnimating) {
174            mAnimating = true;
175            final ObjectAnimator height = ObjectAnimator.ofInt(this, "animatedHeight", mHeight, 0);
176            setMinimumHeight(mHeight);
177            mWidth = getWidth();
178            height.setInterpolator(new DecelerateInterpolator(1.75f));
179            height.setDuration(sShrinkAnimationDuration);
180            height.addListener(listener);
181            height.start();
182        }
183    }
184
185    /**
186     * Set the alpha value for the text displayed by this item.
187     */
188    public void setTextAlpha(float alpha) {
189        if (mSwipeableContent.getAlpha() > TRANSPARENT) {
190            mSwipeableContent.setAlpha(alpha);
191        }
192    }
193
194    /**
195     * Kick off the animation to fade in the leave behind text.
196     * @param delay Whether to delay the start of the animation or not.
197     */
198    public void startFadeInTextAnimation(int delay) {
199        // If this thing isn't already fully visible AND its not already animating...
200        if (!mFadingInText && mSwipeableContent.getAlpha() != OPAQUE) {
201            mFadingInText = true;
202            mFadeIn = startFadeInTextAnimation(mSwipeableContent, delay);
203        }
204    }
205
206    /**
207     * Creates and starts the animator for the fade-in text
208     * @param delay The delay, in milliseconds, before starting the animation
209     * @return The {@link ObjectAnimator}
210     */
211    public static ObjectAnimator startFadeInTextAnimation(final View view, final int delay) {
212        loadStatics(view.getContext());
213
214        final float start = TRANSPARENT;
215        final float end = OPAQUE;
216        final ObjectAnimator fadeIn = ObjectAnimator.ofFloat(view, "alpha", start, end);
217        view.setAlpha(TRANSPARENT);
218        if (delay != 0) {
219            fadeIn.setStartDelay(delay);
220        }
221        fadeIn.setInterpolator(new DecelerateInterpolator(OPAQUE));
222        fadeIn.setDuration(sFadeInAnimationDuration / 2);
223        fadeIn.start();
224
225        return fadeIn;
226    }
227
228    /**
229     * Increase the overall time before fading in a the text description this view.
230     * @param newDelay Amount of total delay the user should see
231     */
232    public void increaseFadeInDelay(int newDelay) {
233        // If this thing isn't already fully visible AND its not already animating...
234        if (!mFadingInText && mSwipeableContent.getAlpha() != OPAQUE) {
235            mFadingInText = true;
236            long delay = mFadeIn.getStartDelay();
237            if (newDelay == delay || mFadeIn.isRunning()) {
238                return;
239            }
240            mFadeIn.cancel();
241            mFadeIn.setStartDelay(newDelay - delay);
242            mFadeIn.start();
243        }
244    }
245
246    /**
247     * Cancel fading in the text description for this view.
248     */
249    public void cancelFadeInTextAnimation() {
250        if (mFadeIn != null) {
251            mFadingInText = false;
252            mFadeIn.cancel();
253        }
254    }
255
256    /**
257     * Cancel fading in the text description for this view only if it the
258     * animation hasn't already started.
259     * @return whether the animation was cancelled
260     */
261    public boolean cancelFadeInTextAnimationIfNotStarted() {
262        // The animation was started, so don't cancel and restart it.
263        if (mFadeIn != null && !mFadeIn.isRunning()) {
264            cancelFadeInTextAnimation();
265            return true;
266        }
267        return false;
268    }
269
270    public void setData(Conversation conversation) {
271        mData = conversation;
272    }
273
274    public Conversation getData() {
275        return mData;
276    }
277
278    @Override
279    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
280        if (mAnimatedHeight != -1) {
281            setMeasuredDimension(mWidth, mAnimatedHeight);
282        } else {
283            // override the height MeasureSpec to ensure this is sized up at the desired height
284            super.onMeasure(widthMeasureSpec,
285                    MeasureSpec.makeMeasureSpec(mHeight, MeasureSpec.EXACTLY));
286        }
287    }
288
289    // Used by animator
290    @SuppressWarnings("unused")
291    public void setAnimatedHeight(int height) {
292        mAnimatedHeight = height;
293        requestLayout();
294    }
295
296    @Override
297    public float getMinAllowScrollDistance() {
298        return sScrollSlop;
299    }
300
301    public void makeInert() {
302        if (mFadeIn != null) {
303            mFadeIn.cancel();
304        }
305        mSwipeableContent.setVisibility(View.GONE);
306        mInert = true;
307    }
308
309    public void cancelFadeOutText() {
310        mSwipeableContent.setAlpha(OPAQUE);
311    }
312
313    public boolean isAnimating() {
314        return this.mFadingInText;
315    }
316}