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.Activity;
     23 import android.app.LoaderManager;
     24 import android.content.ContentResolver;
     25 import android.content.Context;
     26 import android.content.res.Resources;
     27 import android.os.Bundle;
     28 import android.text.SpannableString;
     29 import android.text.TextUtils;
     30 import android.text.style.TextAppearanceSpan;
     31 import android.util.AttributeSet;
     32 import android.view.View;
     33 import android.view.animation.DecelerateInterpolator;
     34 import android.widget.FrameLayout;
     35 import android.widget.TextView;
     36 
     37 import com.android.mail.R;
     38 import com.android.mail.analytics.Analytics;
     39 import com.android.mail.browse.ConversationCursor;
     40 import com.android.mail.preferences.AccountPreferences;
     41 import com.android.mail.preferences.MailPrefs;
     42 import com.android.mail.providers.Account;
     43 import com.android.mail.providers.Folder;
     44 import com.android.mail.utils.LogTag;
     45 import com.android.mail.utils.LogUtils;
     46 import com.android.mail.utils.Utils;
     47 
     48 /**
     49  * A tip displayed on top of conversation view to indicate that Gmail sync is
     50  * currently disabled on this account.
     51  */
     52 public class ConversationSyncDisabledTipView extends FrameLayout
     53         implements ConversationSpecialItemView, SwipeableItemView {
     54 
     55     private static final String LOG_TAG = LogTag.getLogTag();
     56 
     57     private static int sScrollSlop = 0;
     58     private static int sShrinkAnimationDuration;
     59 
     60     private Account mAccount = null;
     61     private Folder mFolder = null;
     62     private final MailPrefs mMailPrefs;
     63     private AccountPreferences mAccountPreferences;
     64     private AnimatedAdapter mAdapter;
     65     private Activity mActivity;
     66 
     67     private View mSwipeableContent;
     68     private TextView mText1;
     69     private TextView mText2;
     70     private View mTextArea;
     71     private SpannableString mEnableSyncInAccountSettingsText;
     72     private final OnClickListener mAutoSyncOffTextClickedListener;
     73     private final OnClickListener mAccountSyncOffTextClickedListener;
     74 
     75     private int mAnimatedHeight = -1;
     76 
     77     private int mReasonSyncOff = ReasonSyncOff.NONE;
     78 
     79     private View mTeaserRightEdge;
     80     /** Whether we are on a tablet device or not */
     81     private final boolean mTabletDevice;
     82     /** When in conversation mode, true if the list is hidden */
     83     private final boolean mListCollapsible;
     84 
     85     public interface ReasonSyncOff {
     86         // Background sync is enabled for current account, do not display this tip
     87         public static final int NONE = 0;
     88         // Global auto-sync (affects all apps and all accounts) is turned off
     89         public static final int AUTO_SYNC_OFF = 1;
     90         // Global auto-sync is on, but Gmail app level sync is disabled for this particular account
     91         public static final int ACCOUNT_SYNC_OFF = 2;
     92     }
     93 
     94     public ConversationSyncDisabledTipView(final Context context) {
     95         this(context, null);
     96     }
     97 
     98     public ConversationSyncDisabledTipView(final Context context, final AttributeSet attrs) {
     99         this(context, attrs, -1);
    100     }
    101 
    102     public ConversationSyncDisabledTipView(
    103             final Context context, final AttributeSet attrs, final int defStyle) {
    104         super(context, attrs, defStyle);
    105 
    106         final Resources resources = context.getResources();
    107 
    108         if (sScrollSlop == 0) {
    109             sScrollSlop = resources.getInteger(R.integer.swipeScrollSlop);
    110             sShrinkAnimationDuration = resources.getInteger(
    111                     R.integer.shrink_animation_duration);
    112         }
    113 
    114         mMailPrefs = MailPrefs.get(context);
    115 
    116         mAutoSyncOffTextClickedListener = new OnClickListener() {
    117             @Override
    118             public void onClick(View v) {
    119                 final TurnAutoSyncOnDialog dialog = TurnAutoSyncOnDialog.newInstance(
    120                         mAccount.getAccountManagerAccount(), mAccount.syncAuthority);
    121                 dialog.show(mActivity.getFragmentManager(), TurnAutoSyncOnDialog.DIALOG_TAG);
    122             }
    123         };
    124 
    125         mAccountSyncOffTextClickedListener = new OnClickListener() {
    126             @Override
    127             public void onClick(View v) {
    128                 Utils.showAccountSettings(getContext(), mAccount);
    129             }
    130         };
    131 
    132         // Create the "Turn on in Account settings." text where "Account settings" appear as
    133         // a blue link.
    134         final String subString = resources.getString(R.string.account_settings_param);
    135         final String entireString = resources.getString(
    136                 R.string.enable_sync_in_account_settings, subString);
    137         mEnableSyncInAccountSettingsText = new SpannableString(entireString);
    138         final int index = entireString.indexOf(subString);
    139         mEnableSyncInAccountSettingsText.setSpan(
    140                 new TextAppearanceSpan(context, R.style.LinksInTipTextAppearance),
    141                 index,
    142                 index + subString.length(),
    143                 0);
    144 
    145         mTabletDevice = Utils.useTabletUI(resources);
    146         mListCollapsible = resources.getBoolean(R.bool.list_collapsible);
    147     }
    148 
    149     public void bindAccount(Account account, ControllableActivity activity) {
    150         mAccount = account;
    151         mAccountPreferences = AccountPreferences.get(getContext(), account);
    152         mActivity = (Activity) activity;
    153     }
    154 
    155     @Override
    156     public void onGetView() {
    157         // Do nothing
    158     }
    159 
    160     @Override
    161     protected void onFinishInflate() {
    162         mSwipeableContent = findViewById(R.id.swipeable_content);
    163 
    164         mText1 = (TextView) findViewById(R.id.text_line1);
    165         mText2 = (TextView) findViewById(R.id.text_line2);
    166         mTextArea = findViewById(R.id.text_area);
    167 
    168         findViewById(R.id.dismiss_button).setOnClickListener(new OnClickListener() {
    169             @Override
    170             public void onClick(View v) {
    171                 dismiss();
    172             }
    173         });
    174 
    175         mTeaserRightEdge = findViewById(R.id.teaser_right_edge);
    176     }
    177 
    178     @Override
    179     public void onUpdate(Folder folder, ConversationCursor cursor) {
    180         mFolder = folder;
    181     }
    182 
    183     @Override
    184     public boolean getShouldDisplayInList() {
    185         if (mAccount == null || mAccount.syncAuthority == null) {
    186             return false;
    187         }
    188 
    189         // Do not show this message for folders/labels that are not set to sync.
    190         if (mFolder == null || mFolder.syncWindow <= 0) {
    191             return false;
    192         }
    193 
    194         setReasonSyncOff(calculateReasonSyncOff(mMailPrefs, mAccount, mAccountPreferences));
    195 
    196         if (mReasonSyncOff != ReasonSyncOff.NONE) {
    197             LogUtils.i(LOG_TAG, "Sync is off with reason %d", mReasonSyncOff);
    198         }
    199 
    200         switch (mReasonSyncOff) {
    201             case ReasonSyncOff.AUTO_SYNC_OFF:
    202                 return (mMailPrefs.getNumOfDismissesForAutoSyncOff() == 0);
    203             case ReasonSyncOff.ACCOUNT_SYNC_OFF:
    204                 return (mAccountPreferences.getNumOfDismissesForAccountSyncOff() == 0);
    205             default:
    206                 return false;
    207         }
    208     }
    209 
    210     public static int calculateReasonSyncOff(MailPrefs mailPrefs,
    211             Account account, AccountPreferences accountPreferences) {
    212         if (!ContentResolver.getMasterSyncAutomatically()) {
    213             // Global sync is turned off
    214             accountPreferences.resetNumOfDismissesForAccountSyncOff();
    215             // Logging to track down bug where this tip is being showing when it shouldn't be.
    216             LogUtils.i(LOG_TAG, "getMasterSyncAutomatically() return false");
    217             return ReasonSyncOff.AUTO_SYNC_OFF;
    218         } else {
    219             // Global sync is on, clear the number of times users has dismissed this
    220             // warning so that next time global sync is off, warning gets displayed again.
    221             mailPrefs.resetNumOfDismissesForAutoSyncOff();
    222 
    223             // Now check for whether account level sync is on/off.
    224             android.accounts.Account acct = account.getAccountManagerAccount();
    225             if (!TextUtils.isEmpty(account.syncAuthority) &&
    226                     !ContentResolver.getSyncAutomatically(acct, account.syncAuthority)) {
    227                 // Account level sync is off
    228                 return ReasonSyncOff.ACCOUNT_SYNC_OFF;
    229             } else {
    230                 // Account sync is on, clear the number of times users has dismissed this
    231                 // warning so that next time sync is off, warning gets displayed again.
    232                 accountPreferences.resetNumOfDismissesForAccountSyncOff();
    233                 return ReasonSyncOff.NONE;
    234             }
    235         }
    236     }
    237 
    238     private void setReasonSyncOff(int reason) {
    239         if (mReasonSyncOff != reason) {
    240             mReasonSyncOff = reason;
    241             switch (mReasonSyncOff) {
    242                 case ReasonSyncOff.AUTO_SYNC_OFF:
    243                     mText1.setText(R.string.auto_sync_off);
    244                     mText2.setText(R.string.tap_to_enable_sync);
    245                     mText2.setVisibility(View.VISIBLE);
    246                     mTextArea.setClickable(true);
    247                     mTextArea.setOnClickListener(mAutoSyncOffTextClickedListener);
    248                     break;
    249                 case ReasonSyncOff.ACCOUNT_SYNC_OFF:
    250                     mText1.setText(R.string.account_sync_off);
    251                     mText2.setText(mEnableSyncInAccountSettingsText);
    252                     mText2.setVisibility(View.VISIBLE);
    253                     mTextArea.setClickable(true);
    254                     mTextArea.setOnClickListener(mAccountSyncOffTextClickedListener);
    255                     break;
    256                 default:
    257                     // Doesn't matter what mText is since this view is not displayed
    258             }
    259         }
    260     }
    261 
    262     @Override
    263     public int getPosition() {
    264         // We want this teaser to go before the first real conversation
    265         return 0;
    266     }
    267 
    268     @Override
    269     public void setAdapter(AnimatedAdapter adapter) {
    270         mAdapter = adapter;
    271     }
    272 
    273     @Override
    274     public void bindFragment(LoaderManager loaderManager, final Bundle savedInstanceState) {
    275     }
    276 
    277     @Override
    278     public void cleanup() {
    279     }
    280 
    281     @Override
    282     public void onConversationSelected() {
    283         // DO NOTHING
    284     }
    285 
    286     @Override
    287     public void onCabModeEntered() {
    288     }
    289 
    290     @Override
    291     public void onCabModeExited() {
    292         // Do nothing
    293     }
    294 
    295     @Override
    296     public void onConversationListVisibilityChanged(final boolean visible) {
    297         // Do nothing
    298     }
    299 
    300     @Override
    301     public void saveInstanceState(final Bundle outState) {
    302         // Do nothing
    303     }
    304 
    305     @Override
    306     public boolean acceptsUserTaps() {
    307         return true;
    308     }
    309 
    310     @Override
    311     public void dismiss() {
    312         final String reason;
    313         switch (mReasonSyncOff) {
    314             case ReasonSyncOff.AUTO_SYNC_OFF:
    315                 mMailPrefs.incNumOfDismissesForAutoSyncOff();
    316                 reason = "auto_sync_off";
    317                 break;
    318             case ReasonSyncOff.ACCOUNT_SYNC_OFF:
    319                 mAccountPreferences.incNumOfDismissesForAccountSyncOff();
    320                 reason = "account_sync_off";
    321                 break;
    322             default:
    323                 reason = null;
    324                 break;
    325         }
    326         Analytics.getInstance().sendEvent("list_swipe", "sync_disabled_tip", reason, 0);
    327         startDestroyAnimation();
    328     }
    329 
    330     @Override
    331     public SwipeableView getSwipeableView() {
    332         return SwipeableView.from(mSwipeableContent);
    333     }
    334 
    335     @Override
    336     public boolean canChildBeDismissed() {
    337         return true;
    338     }
    339 
    340     @Override
    341     public float getMinAllowScrollDistance() {
    342         return sScrollSlop;
    343     }
    344 
    345     private void startDestroyAnimation() {
    346         final int start = getHeight();
    347         final int end = 0;
    348         mAnimatedHeight = start;
    349         final ObjectAnimator heightAnimator =
    350                 ObjectAnimator.ofInt(this, "animatedHeight", start, end);
    351         heightAnimator.setInterpolator(new DecelerateInterpolator(2.0f));
    352         heightAnimator.setDuration(sShrinkAnimationDuration);
    353         heightAnimator.addListener(new AnimatorListener() {
    354             @Override
    355             public void onAnimationStart(final Animator animation) {
    356                 // Do nothing
    357             }
    358 
    359             @Override
    360             public void onAnimationRepeat(final Animator animation) {
    361                 // Do nothing
    362             }
    363 
    364             @Override
    365             public void onAnimationEnd(final Animator animation) {
    366                 // We should no longer exist, so notify the adapter
    367                 mAdapter.notifyDataSetChanged();
    368             }
    369 
    370             @Override
    371             public void onAnimationCancel(final Animator animation) {
    372                 // Do nothing
    373             }
    374         });
    375         heightAnimator.start();
    376     }
    377 
    378     /**
    379      * This method is used by the animator.  It is explicitly kept in proguard.flags to prevent it
    380      * from being removed, inlined, or obfuscated.
    381      * Edit ./vendor/unbundled/packages/apps/UnifiedGmail/proguard.flags
    382      * In the future, we want to use @Keep
    383      */
    384     public void setAnimatedHeight(final int height) {
    385         mAnimatedHeight = height;
    386         requestLayout();
    387     }
    388 
    389     @Override
    390     protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
    391         if (Utils.getDisplayListRightEdgeEffect(mTabletDevice, mListCollapsible,
    392                 mAdapter.getViewMode())) {
    393             mTeaserRightEdge.setVisibility(VISIBLE);
    394         } else {
    395             mTeaserRightEdge.setVisibility(GONE);
    396         }
    397 
    398         if (mAnimatedHeight == -1) {
    399             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    400         } else {
    401             setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mAnimatedHeight);
    402         }
    403     }
    404 
    405     @Override
    406     public boolean commitLeaveBehindItem() {
    407         // This view has no leave-behind
    408         return false;
    409     }
    410 }
    411