• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.mail.ui;
18 
19 import android.animation.Animator;
20 import android.animation.ObjectAnimator;
21 import android.animation.Animator.AnimatorListener;
22 import android.app.LoaderManager;
23 import android.app.LoaderManager.LoaderCallbacks;
24 import android.content.Context;
25 import android.content.Loader;
26 import android.content.res.Resources;
27 import android.os.Bundle;
28 import android.text.SpannableString;
29 import android.text.style.TextAppearanceSpan;
30 import android.util.AttributeSet;
31 import android.view.View;
32 import android.view.animation.DecelerateInterpolator;
33 import android.widget.FrameLayout;
34 import android.widget.TextView;
35 
36 import com.android.mail.R;
37 import com.android.mail.browse.ConversationCursor;
38 import com.android.mail.content.ObjectCursor;
39 import com.android.mail.content.ObjectCursorLoader;
40 import com.android.mail.preferences.AccountPreferences;
41 import com.android.mail.providers.Account;
42 import com.android.mail.providers.Folder;
43 import com.android.mail.providers.UIProvider;
44 import 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  */
50 public 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 
ConversationsInOutboxTipView(final Context context)78     public ConversationsInOutboxTipView(final Context context) {
79         this(context, null);
80     }
81 
ConversationsInOutboxTipView(final Context context, final AttributeSet attrs)82     public ConversationsInOutboxTipView(final Context context, final AttributeSet attrs) {
83         this(context, attrs, -1);
84     }
85 
ConversationsInOutboxTipView( final Context context, final AttributeSet attrs, final int defStyle)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 
bind(final Account account, final FolderSelector folderSelector)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
onGetView()109     public void onGetView() {
110         // Do nothing
111     }
112 
113     @Override
onFinishInflate()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 
goToOutbox()136     private void goToOutbox() {
137         if (mOutbox != null) {
138             mFolderSelector.onFolderSelected(mOutbox);
139         }
140     }
141 
142     @Override
onUpdate(Folder folder, ConversationCursor cursor)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 
onOutboxTotalCount(int outboxCount)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 
updateText()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
getShouldDisplayInList()219     public boolean getShouldDisplayInList() {
220         return (mOutboxCount > 0 && mOutboxCount != mAccountPreferences.getLastSeenOutboxCount());
221     }
222 
223     @Override
getPosition()224     public int getPosition() {
225         // We want this teaser to go before the first real conversation
226         return 0;
227     }
228 
229     @Override
setAdapter(AnimatedAdapter adapter)230     public void setAdapter(AnimatedAdapter adapter) {
231         mAdapter = adapter;
232     }
233 
234     @Override
bindFragment(final LoaderManager loaderManager, final Bundle savedInstanceState)235     public void bindFragment(final LoaderManager loaderManager, final Bundle savedInstanceState) {
236         mLoaderManager = loaderManager;
237     }
238 
239     @Override
cleanup()240     public void cleanup() {
241     }
242 
243     @Override
onConversationSelected()244     public void onConversationSelected() {
245         // DO NOTHING
246     }
247 
248     @Override
onCabModeEntered()249     public void onCabModeEntered() {
250     }
251 
252     @Override
onCabModeExited()253     public void onCabModeExited() {
254     }
255 
256     @Override
onConversationListVisibilityChanged(final boolean visible)257     public void onConversationListVisibilityChanged(final boolean visible) {
258         // Do nothing
259     }
260 
261     @Override
saveInstanceState(final Bundle outState)262     public void saveInstanceState(final Bundle outState) {
263         // Do nothing
264     }
265 
266     @Override
acceptsUserTaps()267     public boolean acceptsUserTaps() {
268         return true;
269     }
270 
271     @Override
dismiss()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
getSwipeableView()286     public SwipeableView getSwipeableView() {
287         return SwipeableView.from(mSwipeableContent);
288     }
289 
290     @Override
canChildBeDismissed()291     public boolean canChildBeDismissed() {
292         return true;
293     }
294 
295     @Override
getMinAllowScrollDistance()296     public float getMinAllowScrollDistance() {
297         return sScrollSlop;
298     }
299 
startDestroyAnimation()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      */
setAnimatedHeight(final int height)339     public void setAnimatedHeight(final int height) {
340         mAnimatedHeight = height;
341         requestLayout();
342     }
343 
344     @Override
onMeasure(final int widthMeasureSpec, final int heightMeasureSpec)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