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        }
107    }
108
109    public void bind(int pos, Account account, AnimatedAdapter adapter,
110            ToastBarOperation undoOp, Conversation target, Folder folder, int height) {
111        position = pos;
112        mUndoOp = undoOp;
113        mAccount = account;
114        mAdapter = adapter;
115        mHeight = height;
116        setData(target);
117        mSwipeableContent = findViewById(R.id.swipeable_content);
118        // Listen on swipeable content so that we can show both the undo icon
119        // and button text as selected since they set duplicateParentState to true
120        mSwipeableContent.setOnClickListener(this);
121        mSwipeableContent.setAlpha(TRANSPARENT);
122        mText = ((TextView) findViewById(R.id.undo_description_text));
123        mText.setText(Utils.convertHtmlToPlainText(mUndoOp
124                .getSingularDescription(getContext(), folder)));
125        mText.setOnClickListener(this);
126    }
127
128    public void commit() {
129        ConversationCursor cursor = mAdapter.getConversationCursor();
130        if (cursor != null) {
131            cursor.delete(ImmutableList.of(getData()));
132        }
133    }
134
135    @Override
136    public void dismiss() {
137        if (mAdapter != null) {
138            Analytics.getInstance().sendEvent("list_swipe", "leave_behind", null, 0);
139            mAdapter.fadeOutSpecificLeaveBehindItem(mData.id);
140            mAdapter.notifyDataSetChanged();
141        }
142    }
143
144    public long getConversationId() {
145        return getData().id;
146    }
147
148    @Override
149    public SwipeableView getSwipeableView() {
150        return SwipeableView.from(mSwipeableContent);
151    }
152
153    @Override
154    public boolean canChildBeDismissed() {
155        return !mInert;
156    }
157
158    public LeaveBehindData getLeaveBehindData() {
159        return new LeaveBehindData(getData(), mUndoOp, mHeight);
160    }
161
162    /**
163     * Animate shrinking the height of this view.
164     * @param listener the method to call when the animation is done
165     */
166    public void startShrinkAnimation(AnimatorListener listener) {
167        if (!mAnimating) {
168            mAnimating = true;
169            final ObjectAnimator height = ObjectAnimator.ofInt(this, "animatedHeight", mHeight, 0);
170            setMinimumHeight(mHeight);
171            mWidth = getWidth();
172            height.setInterpolator(new DecelerateInterpolator(1.75f));
173            height.setDuration(sShrinkAnimationDuration);
174            height.addListener(listener);
175            height.start();
176        }
177    }
178
179    /**
180     * Set the alpha value for the text displayed by this item.
181     */
182    public void setTextAlpha(float alpha) {
183        if (mSwipeableContent.getAlpha() > TRANSPARENT) {
184            mSwipeableContent.setAlpha(alpha);
185        }
186    }
187
188    /**
189     * Kick off the animation to fade in the leave behind text.
190     * @param delay Whether to delay the start of the animation or not.
191     */
192    public void startFadeInTextAnimation(int delay) {
193        // If this thing isn't already fully visible AND its not already animating...
194        if (!mFadingInText && mSwipeableContent.getAlpha() != OPAQUE) {
195            mFadingInText = true;
196            mFadeIn = startFadeInTextAnimation(mSwipeableContent, delay);
197        }
198    }
199
200    /**
201     * Creates and starts the animator for the fade-in text
202     * @param delay The delay, in milliseconds, before starting the animation
203     * @return The {@link ObjectAnimator}
204     */
205    public static ObjectAnimator startFadeInTextAnimation(final View view, final int delay) {
206        loadStatics(view.getContext());
207
208        final float start = TRANSPARENT;
209        final float end = OPAQUE;
210        final ObjectAnimator fadeIn = ObjectAnimator.ofFloat(view, "alpha", start, end);
211        view.setAlpha(TRANSPARENT);
212        if (delay != 0) {
213            fadeIn.setStartDelay(delay);
214        }
215        fadeIn.setInterpolator(new DecelerateInterpolator(OPAQUE));
216        fadeIn.setDuration(sFadeInAnimationDuration / 2);
217        fadeIn.start();
218
219        return fadeIn;
220    }
221
222    /**
223     * Increase the overall time before fading in a the text description this view.
224     * @param newDelay Amount of total delay the user should see
225     */
226    public void increaseFadeInDelay(int newDelay) {
227        // If this thing isn't already fully visible AND its not already animating...
228        if (!mFadingInText && mSwipeableContent.getAlpha() != OPAQUE) {
229            mFadingInText = true;
230            long delay = mFadeIn.getStartDelay();
231            if (newDelay == delay || mFadeIn.isRunning()) {
232                return;
233            }
234            mFadeIn.cancel();
235            mFadeIn.setStartDelay(newDelay - delay);
236            mFadeIn.start();
237        }
238    }
239
240    /**
241     * Cancel fading in the text description for this view.
242     */
243    public void cancelFadeInTextAnimation() {
244        if (mFadeIn != null) {
245            mFadingInText = false;
246            mFadeIn.cancel();
247        }
248    }
249
250    /**
251     * Cancel fading in the text description for this view only if it the
252     * animation hasn't already started.
253     * @return whether the animation was cancelled
254     */
255    public boolean cancelFadeInTextAnimationIfNotStarted() {
256        // The animation was started, so don't cancel and restart it.
257        if (mFadeIn != null && !mFadeIn.isRunning()) {
258            cancelFadeInTextAnimation();
259            return true;
260        }
261        return false;
262    }
263
264    public void setData(Conversation conversation) {
265        mData = conversation;
266    }
267
268    public Conversation getData() {
269        return mData;
270    }
271
272    @Override
273    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
274        if (mAnimatedHeight != -1) {
275            setMeasuredDimension(mWidth, mAnimatedHeight);
276        } else {
277            // override the height MeasureSpec to ensure this is sized up at the desired height
278            super.onMeasure(widthMeasureSpec,
279                    MeasureSpec.makeMeasureSpec(mHeight, MeasureSpec.EXACTLY));
280        }
281    }
282
283    // Used by animator
284    @SuppressWarnings("unused")
285    public void setAnimatedHeight(int height) {
286        mAnimatedHeight = height;
287        requestLayout();
288    }
289
290    @Override
291    public float getMinAllowScrollDistance() {
292        return sScrollSlop;
293    }
294
295    public void makeInert() {
296        if (mFadeIn != null) {
297            mFadeIn.cancel();
298        }
299        mSwipeableContent.setVisibility(View.GONE);
300        mInert = true;
301    }
302
303    public void cancelFadeOutText() {
304        mSwipeableContent.setAlpha(OPAQUE);
305    }
306
307    public boolean isAnimating() {
308        return this.mFadingInText;
309    }
310}