1/*
2 * Copyright (C) 2013 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.mail.ui;
18
19import android.animation.Animator;
20import android.animation.ObjectAnimator;
21import android.animation.Animator.AnimatorListener;
22import android.app.LoaderManager;
23import android.app.LoaderManager.LoaderCallbacks;
24import android.content.Context;
25import android.content.Loader;
26import android.content.res.Resources;
27import android.os.Bundle;
28import android.text.SpannableString;
29import android.text.style.TextAppearanceSpan;
30import android.util.AttributeSet;
31import android.view.View;
32import android.view.animation.DecelerateInterpolator;
33import android.widget.FrameLayout;
34import android.widget.TextView;
35
36import com.android.mail.R;
37import com.android.mail.browse.ConversationCursor;
38import com.android.mail.content.ObjectCursor;
39import com.android.mail.content.ObjectCursorLoader;
40import com.android.mail.preferences.AccountPreferences;
41import com.android.mail.providers.Account;
42import com.android.mail.providers.Folder;
43import com.android.mail.providers.UIProvider;
44import com.android.mail.utils.Utils;
45
46/**
47 * Tip that is displayed in conversation list of 'Sent' folder whenever there are
48 * one or more messages in the Outbox.
49 */
50public class ConversationsInOutboxTipView extends FrameLayout
51        implements ConversationSpecialItemView, SwipeableItemView {
52
53    private static int sScrollSlop = 0;
54    private static int sShrinkAnimationDuration;
55
56    private Account mAccount = null;
57    private AccountPreferences mAccountPreferences;
58    private AnimatedAdapter mAdapter;
59    private LoaderManager mLoaderManager;
60    private FolderSelector mFolderSelector;
61    private Folder mOutbox;
62    private int mOutboxCount = -1;
63
64    private View mSwipeableContent;
65    private TextView mText;
66
67    private int mAnimatedHeight = -1;
68
69    private View mTeaserRightEdge;
70    /** Whether we are on a tablet device or not */
71    private final boolean mTabletDevice;
72    /** When in conversation mode, true if the list is hidden */
73    private final boolean mListCollapsible;
74
75    private static final int LOADER_FOLDER_LIST =
76            AbstractActivityController.LAST_FRAGMENT_LOADER_ID + 100;
77
78    public ConversationsInOutboxTipView(final Context context) {
79        this(context, null);
80    }
81
82    public ConversationsInOutboxTipView(final Context context, final AttributeSet attrs) {
83        this(context, attrs, -1);
84    }
85
86    public ConversationsInOutboxTipView(
87            final Context context, final AttributeSet attrs, final int defStyle) {
88        super(context, attrs, defStyle);
89
90        final Resources resources = context.getResources();
91
92        if (sScrollSlop == 0) {
93            sScrollSlop = resources.getInteger(R.integer.swipeScrollSlop);
94            sShrinkAnimationDuration = resources.getInteger(
95                    R.integer.shrink_animation_duration);
96        }
97
98        mTabletDevice = Utils.useTabletUI(resources);
99        mListCollapsible = resources.getBoolean(R.bool.list_collapsible);
100    }
101
102    public void bind(final Account account, final FolderSelector folderSelector) {
103        mAccount = account;
104        mAccountPreferences = AccountPreferences.get(getContext(), account.getEmailAddress());
105        mFolderSelector = folderSelector;
106    }
107
108    @Override
109    public void onGetView() {
110        // Do nothing
111    }
112
113    @Override
114    protected void onFinishInflate() {
115        mSwipeableContent = findViewById(R.id.swipeable_content);
116
117        mText = (TextView) findViewById(R.id.outbox);
118
119        findViewById(R.id.outbox).setOnClickListener(new View.OnClickListener() {
120            @Override
121            public void onClick(View v) {
122                goToOutbox();
123            }
124        });
125
126        findViewById(R.id.dismiss_button).setOnClickListener(new View.OnClickListener() {
127            @Override
128            public void onClick(View v) {
129                dismiss();
130            }
131        });
132
133        mTeaserRightEdge = findViewById(R.id.teaser_right_edge);
134    }
135
136    private void goToOutbox() {
137        if (mOutbox != null) {
138            mFolderSelector.onFolderSelected(mOutbox);
139        }
140    }
141
142    @Override
143    public void onUpdate(Folder folder, ConversationCursor cursor) {
144        if (mLoaderManager != null && folder != null) {
145            if ((folder.type & UIProvider.FolderType.SENT) > 0) {
146                // Only display this tip if user is viewing the Sent folder
147                mLoaderManager.initLoader(LOADER_FOLDER_LIST, null, mFolderListLoaderCallbacks);
148            }
149        }
150    }
151
152    private final LoaderCallbacks<ObjectCursor<Folder>> mFolderListLoaderCallbacks =
153            new LoaderManager.LoaderCallbacks<ObjectCursor<Folder>>() {
154        @Override
155        public void onLoaderReset(final Loader<ObjectCursor<Folder>> loader) {
156            // Do nothing
157        }
158
159        @Override
160        public void onLoadFinished(final Loader<ObjectCursor<Folder>> loader,
161                final ObjectCursor<Folder> data) {
162            if (data != null && data.moveToFirst()) {
163                do {
164                    final Folder folder = data.getModel();
165                    if ((folder.type & UIProvider.FolderType.OUTBOX) > 0) {
166                        mOutbox = folder;
167                        onOutboxTotalCount(folder.totalCount);
168                    }
169                } while (data.moveToNext());
170            }
171        }
172
173        @Override
174        public Loader<ObjectCursor<Folder>> onCreateLoader(final int id, final Bundle args) {
175            // This loads all folders in order to find 'Outbox'.  We could consider adding a new
176            // query to load folders of a given type to make this more efficient, but should be
177            // okay for now since this is triggered infrequently (only when user visits the
178            // 'Sent' folder).
179            final ObjectCursorLoader<Folder> loader = new ObjectCursorLoader<Folder>(getContext(),
180                    mAccount.folderListUri, UIProvider.FOLDERS_PROJECTION, Folder.FACTORY);
181            return loader;
182        }
183    };
184
185    private void onOutboxTotalCount(int outboxCount) {
186        if (mOutboxCount != outboxCount) {
187            mOutboxCount = outboxCount;
188            if (outboxCount > 0) {
189                if (mText != null) {
190                    updateText();
191                }
192            }
193        }
194        if (outboxCount == 0) {
195            // Clear the last seen count, so that new messages in Outbox will always cause this
196            // tip to appear again.
197            mAccountPreferences.setLastSeenOutboxCount(0);
198        }
199    }
200
201    private void updateText() {
202        // Update the display text to reflect current mOutboxCount
203        final Resources resources = getContext().getResources();
204        final String subString = mOutbox.name;
205        final String entireString = resources.getString(
206                R.string.unsent_messages_in_outbox,
207                String.valueOf(mOutboxCount), subString);
208        final SpannableString text = new SpannableString(entireString);
209        final int index = entireString.indexOf(subString);
210        text.setSpan(
211                new TextAppearanceSpan(getContext(), R.style.LinksInTipTextAppearance),
212                index,
213                index + subString.length(),
214                0);
215        mText.setText(text);
216    }
217
218    @Override
219    public boolean getShouldDisplayInList() {
220        return (mOutboxCount > 0 && mOutboxCount != mAccountPreferences.getLastSeenOutboxCount());
221    }
222
223    @Override
224    public int getPosition() {
225        // We want this teaser to go before the first real conversation
226        return 0;
227    }
228
229    @Override
230    public void setAdapter(AnimatedAdapter adapter) {
231        mAdapter = adapter;
232    }
233
234    @Override
235    public void bindFragment(final LoaderManager loaderManager, final Bundle savedInstanceState) {
236        mLoaderManager = loaderManager;
237    }
238
239    @Override
240    public void cleanup() {
241    }
242
243    @Override
244    public void onConversationSelected() {
245        // DO NOTHING
246    }
247
248    @Override
249    public void onCabModeEntered() {
250    }
251
252    @Override
253    public void onCabModeExited() {
254    }
255
256    @Override
257    public void onConversationListVisibilityChanged(final boolean visible) {
258        // Do nothing
259    }
260
261    @Override
262    public void saveInstanceState(final Bundle outState) {
263        // Do nothing
264    }
265
266    @Override
267    public boolean acceptsUserTaps() {
268        return true;
269    }
270
271    @Override
272    public void dismiss() {
273        // Do not show this tip again until we have a new count.  Note this is not quite
274        // ideal behavior since after a user dismisses an "1 unsent in outbox" tip,
275        // the message stuck in Outbox could get sent, and a new one gets stuck.
276        // If the user checks back on on Sent folder then, we don't reshow the message since count
277        // itself hasn't changed, but ideally we should since it's a different message than before.
278        // However if user checks the Sent folder in between (when there were 0 messages
279        // in Outbox), the preference is cleared (see {@link onOutboxTotalCount}).
280        mAccountPreferences.setLastSeenOutboxCount(mOutboxCount);
281
282        startDestroyAnimation();
283    }
284
285    @Override
286    public SwipeableView getSwipeableView() {
287        return SwipeableView.from(mSwipeableContent);
288    }
289
290    @Override
291    public boolean canChildBeDismissed() {
292        return true;
293    }
294
295    @Override
296    public float getMinAllowScrollDistance() {
297        return sScrollSlop;
298    }
299
300    private void startDestroyAnimation() {
301        final int start = getHeight();
302        final int end = 0;
303        mAnimatedHeight = start;
304        final ObjectAnimator heightAnimator =
305                ObjectAnimator.ofInt(this, "animatedHeight", start, end);
306        heightAnimator.setInterpolator(new DecelerateInterpolator(2.0f));
307        heightAnimator.setDuration(sShrinkAnimationDuration);
308        heightAnimator.addListener(new AnimatorListener() {
309            @Override
310            public void onAnimationStart(final Animator animation) {
311                // Do nothing
312            }
313
314            @Override
315            public void onAnimationRepeat(final Animator animation) {
316                // Do nothing
317            }
318
319            @Override
320            public void onAnimationEnd(final Animator animation) {
321                // We should no longer exist, so notify the adapter
322                mAdapter.notifyDataSetChanged();
323            }
324
325            @Override
326            public void onAnimationCancel(final Animator animation) {
327                // Do nothing
328            }
329        });
330        heightAnimator.start();
331    }
332
333    /**
334     * This method is used by the animator.  It is explicitly kept in proguard.flags to prevent it
335     * from being removed, inlined, or obfuscated.
336     * Edit ./vendor/unbundled/packages/apps/UnifiedGmail/proguard.flags
337     * In the future, we want to use @Keep
338     */
339    public void setAnimatedHeight(final int height) {
340        mAnimatedHeight = height;
341        requestLayout();
342    }
343
344    @Override
345    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
346        if (Utils.getDisplayListRightEdgeEffect(mTabletDevice, mListCollapsible,
347                mAdapter.getViewMode())) {
348            mTeaserRightEdge.setVisibility(VISIBLE);
349        } else {
350            mTeaserRightEdge.setVisibility(GONE);
351        }
352
353        if (mAnimatedHeight == -1) {
354            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
355        } else {
356            setMeasuredDimension(View.MeasureSpec.getSize(widthMeasureSpec), mAnimatedHeight);
357        }
358    }
359}
360