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.Activity;
23import android.app.LoaderManager;
24import android.content.ContentResolver;
25import android.content.Context;
26import android.content.res.Resources;
27import android.os.Bundle;
28import android.text.SpannableString;
29import android.text.TextUtils;
30import android.text.style.TextAppearanceSpan;
31import android.util.AttributeSet;
32import android.view.View;
33import android.view.animation.DecelerateInterpolator;
34import android.widget.FrameLayout;
35import android.widget.TextView;
36
37import com.android.mail.R;
38import com.android.mail.analytics.Analytics;
39import com.android.mail.browse.ConversationCursor;
40import com.android.mail.preferences.AccountPreferences;
41import com.android.mail.preferences.MailPrefs;
42import com.android.mail.providers.Account;
43import com.android.mail.providers.Folder;
44import com.android.mail.utils.LogTag;
45import com.android.mail.utils.LogUtils;
46import 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 */
52public 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.getEmailAddress());
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