Home | History | Annotate | Download | only in ui
      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 
     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