1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.preferences;
19
20import android.content.Context;
21import android.content.SharedPreferences;
22import android.support.annotation.StringDef;
23
24import com.android.mail.R;
25import com.android.mail.providers.Account;
26import com.android.mail.providers.UIProvider;
27import com.android.mail.utils.LogUtils;
28import com.android.mail.widget.BaseWidgetProvider;
29import com.google.common.annotations.VisibleForTesting;
30import com.google.common.collect.ImmutableSet;
31import com.google.common.collect.Sets;
32
33import java.lang.annotation.Retention;
34import java.lang.annotation.RetentionPolicy;
35import java.util.Collections;
36import java.util.List;
37import java.util.Set;
38import java.util.regex.Pattern;
39
40/**
41 * A high-level API to store and retrieve unified mail preferences.
42 * <p>
43 * This will serve as an eventual replacement for Gmail's Persistence class.
44 */
45public final class MailPrefs extends VersionedPrefs {
46
47    public static final boolean SHOW_EXPERIMENTAL_PREFS = false;
48
49    private static final String PREFS_NAME = "UnifiedEmail";
50
51    private static MailPrefs sInstance;
52
53    private final int mSnapHeaderDefault;
54
55    public static final class PreferenceKeys {
56        private static final String MIGRATED_VERSION = "migrated-version";
57
58        public static final String WIDGET_ACCOUNT_PREFIX = "widget-account-";
59
60        /** Hidden preference to indicate what version a "What's New" dialog was last shown for. */
61        public static final String WHATS_NEW_LAST_SHOWN_VERSION = "whats-new-last-shown-version";
62
63        /**
64         * A boolean that, if <code>true</code>, means we should default all replies to "reply all"
65         */
66        public static final String DEFAULT_REPLY_ALL = "default-reply-all";
67        /**
68         * A boolean that, if <code>true</code>, means we should allow conversation list swiping
69         */
70        public static final String CONVERSATION_LIST_SWIPE = "conversation-list-swipe";
71
72        /** A string indicating the user's removal action preference. */
73        public static final String REMOVAL_ACTION = "removal-action";
74
75        /** Hidden preference used to cache the active notification set */
76        private static final String CACHED_ACTIVE_NOTIFICATION_SET =
77                "cache-active-notification-set";
78
79        /**
80         * A string indicating whether the conversation photo teaser has been previously
81         * shown and dismissed. This is the third version of it (thus the three at the end).
82         * Previous versions: "conversation-photo-teaser-shown"
83         * and "conversation-photo-teaser-shown-two".
84         */
85        private static final String
86                CONVERSATION_PHOTO_TEASER_SHOWN = "conversation-photo-teaser-shown-three";
87
88        public static final String DISPLAY_IMAGES = "display_images";
89        public static final String DISPLAY_IMAGES_PATTERNS = "display_sender_images_patterns_set";
90
91
92        public static final String SHOW_SENDER_IMAGES = "conversation-list-sender-image";
93
94        public static final String
95                LONG_PRESS_TO_SELECT_TIP_SHOWN = "long-press-to-select-tip-shown";
96
97        /** @deprecated attachment previews have been removed; avoid future key name conflicts */
98        public static final String EXPERIMENT_AP_PARALLAX_SPEED_ALTERNATIVE = "ap-parallax-speed";
99
100        /** @deprecated attachment previews have been removed; avoid future key name conflicts */
101        public static final String EXPERIMENT_AP_PARALLAX_DIRECTION_ALTERNATIVE
102                = "ap-parallax-direction";
103
104        public static final String GLOBAL_SYNC_OFF_DISMISSES = "num-of-dismisses-auto-sync-off";
105        public static final String AIRPLANE_MODE_ON_DISMISSES = "num-of-dismisses-airplane-mode-on";
106
107        public static final String AUTO_ADVANCE_MODE = "auto-advance-mode";
108
109        public static final String CONFIRM_DELETE = "confirm-delete";
110        public static final String CONFIRM_ARCHIVE = "confirm-archive";
111        public static final String CONFIRM_SEND = "confirm-send";
112
113        public static final String CONVERSATION_OVERVIEW_MODE = "conversation-overview-mode";
114
115        public static final String SNAP_HEADER_MODE = "snap-header-mode";
116
117        public static final String RECENT_ACCOUNTS = "recent-accounts";
118
119        public static final ImmutableSet<String> BACKUP_KEYS =
120                new ImmutableSet.Builder<String>()
121                .add(DEFAULT_REPLY_ALL)
122                .add(CONVERSATION_LIST_SWIPE)
123                .add(REMOVAL_ACTION)
124                .add(DISPLAY_IMAGES)
125                .add(DISPLAY_IMAGES_PATTERNS)
126                .add(SHOW_SENDER_IMAGES)
127                .add(LONG_PRESS_TO_SELECT_TIP_SHOWN)
128                .add(AUTO_ADVANCE_MODE)
129                .add(CONFIRM_DELETE)
130                .add(CONFIRM_ARCHIVE)
131                .add(CONFIRM_SEND)
132                .add(CONVERSATION_OVERVIEW_MODE)
133                .add(SNAP_HEADER_MODE)
134                .build();
135    }
136
137    public static final class ConversationListSwipeActions {
138        public static final String ARCHIVE = "archive";
139        public static final String DELETE = "delete";
140        public static final String DISABLED = "disabled";
141    }
142
143    @Retention(RetentionPolicy.SOURCE)
144    @StringDef({
145            RemovalActions.ARCHIVE,
146            RemovalActions.ARCHIVE_AND_DELETE,
147            RemovalActions.DELETE
148    })
149    public @interface RemovalActionTypes {}
150    public static final class RemovalActions {
151        public static final String ARCHIVE = "archive";
152        public static final String DELETE = "delete";
153        public static final String ARCHIVE_AND_DELETE = "archive-and-delete";
154    }
155
156    public static MailPrefs get(final Context c) {
157        if (sInstance == null) {
158            sInstance = new MailPrefs(c, PREFS_NAME);
159        }
160        return sInstance;
161    }
162
163    @VisibleForTesting
164    public MailPrefs(final Context c, final String prefsName) {
165        super(c, prefsName);
166        mSnapHeaderDefault = c.getResources().getInteger(R.integer.prefDefault_snapHeader);
167    }
168
169    @Override
170    protected void performUpgrade(final int oldVersion, final int newVersion) {
171        if (oldVersion > newVersion) {
172            throw new IllegalStateException(
173                    "You appear to have downgraded your app. Please clear app data.");
174        } else if (oldVersion == newVersion) {
175            return;
176        }
177    }
178
179    @Override
180    protected boolean canBackup(final String key) {
181        return PreferenceKeys.BACKUP_KEYS.contains(key);
182    }
183
184    @Override
185    protected boolean hasMigrationCompleted() {
186        return getSharedPreferences().getInt(PreferenceKeys.MIGRATED_VERSION, 0)
187                >= CURRENT_VERSION_NUMBER;
188    }
189
190    @Override
191    protected void setMigrationComplete() {
192        getEditor().putInt(PreferenceKeys.MIGRATED_VERSION, CURRENT_VERSION_NUMBER).commit();
193    }
194
195    public boolean isWidgetConfigured(int appWidgetId) {
196        return getSharedPreferences().contains(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + appWidgetId);
197    }
198
199    public void configureWidget(int appWidgetId, Account account, final String folderUri) {
200        if (account == null) {
201            LogUtils.e(LOG_TAG, "Cannot configure widget with null account");
202            return;
203        }
204        getEditor().putString(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + appWidgetId,
205                createWidgetPreferenceValue(account, folderUri)).apply();
206    }
207
208    public String getWidgetConfiguration(int appWidgetId) {
209        return getSharedPreferences().getString(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + appWidgetId,
210                null);
211    }
212
213    private static String createWidgetPreferenceValue(Account account, String folderUri) {
214        return account.uri.toString() + BaseWidgetProvider.ACCOUNT_FOLDER_PREFERENCE_SEPARATOR
215                + folderUri;
216
217    }
218
219    public void clearWidgets(int[] appWidgetIds) {
220        for (int id : appWidgetIds) {
221            getEditor().remove(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + id);
222        }
223        getEditor().apply();
224    }
225
226    /** If <code>true</code>, we should default all replies to "reply all" rather than "reply" */
227    public boolean getDefaultReplyAll() {
228        return getSharedPreferences().getBoolean(PreferenceKeys.DEFAULT_REPLY_ALL, false);
229    }
230
231    public void setDefaultReplyAll(final boolean replyAll) {
232        getEditor().putBoolean(PreferenceKeys.DEFAULT_REPLY_ALL, replyAll).apply();
233        notifyBackupPreferenceChanged();
234    }
235
236    /**
237     * Returns a string indicating the preferred removal action.
238     * Should be one of the {@link RemovalActions}.
239     */
240    public String getRemovalAction(final boolean supportsArchive) {
241        if (!supportsArchive) {
242            return RemovalActions.DELETE;
243        }
244
245        final SharedPreferences sharedPreferences = getSharedPreferences();
246        return sharedPreferences.getString(PreferenceKeys.REMOVAL_ACTION,
247                RemovalActions.ARCHIVE_AND_DELETE);
248    }
249
250    /**
251     * Sets the removal action preference.
252     * @param removalAction The preferred {@link RemovalActions}.
253     */
254    public void setRemovalAction(final @RemovalActionTypes String removalAction) {
255        getEditor().putString(PreferenceKeys.REMOVAL_ACTION, removalAction).apply();
256        notifyBackupPreferenceChanged();
257    }
258
259    /**
260     * Gets a boolean indicating whether conversation list swiping is enabled.
261     */
262    public boolean getIsConversationListSwipeEnabled() {
263        final SharedPreferences sharedPreferences = getSharedPreferences();
264        return sharedPreferences.getBoolean(PreferenceKeys.CONVERSATION_LIST_SWIPE, true);
265    }
266
267    public void setConversationListSwipeEnabled(final boolean enabled) {
268        getEditor().putBoolean(PreferenceKeys.CONVERSATION_LIST_SWIPE, enabled).apply();
269        notifyBackupPreferenceChanged();
270    }
271
272    /**
273     * Gets the action to take (one of the values from {@link UIProvider.Swipe}) when an item in the
274     * conversation list is swiped.
275     *
276     * @param allowArchive <code>true</code> if Archive is an acceptable action (this will affect
277     *        the default return value)
278     */
279    public int getConversationListSwipeActionInteger(final boolean allowArchive) {
280        final boolean swipeEnabled = getIsConversationListSwipeEnabled();
281        final boolean archive = !RemovalActions.DELETE.equals(getRemovalAction(allowArchive));
282
283        if (swipeEnabled) {
284            return archive ? UIProvider.Swipe.ARCHIVE : UIProvider.Swipe.DELETE;
285        }
286
287        return UIProvider.Swipe.DISABLED;
288    }
289
290    /**
291     * Returns the previously cached notification set
292     */
293    public Set<String> getActiveNotificationSet() {
294        return getSharedPreferences()
295                .getStringSet(PreferenceKeys.CACHED_ACTIVE_NOTIFICATION_SET, null);
296    }
297
298    /**
299     * Caches the current notification set.
300     */
301    public void cacheActiveNotificationSet(final Set<String> notificationSet) {
302        getEditor().putStringSet(PreferenceKeys.CACHED_ACTIVE_NOTIFICATION_SET, notificationSet)
303                .apply();
304    }
305
306    /**
307     * Returns whether the teaser has been shown before
308     */
309    public boolean isConversationPhotoTeaserAlreadyShown() {
310        return getSharedPreferences()
311                .getBoolean(PreferenceKeys.CONVERSATION_PHOTO_TEASER_SHOWN, false);
312    }
313
314    /**
315     * Notify that we have shown the teaser
316     */
317    public void setConversationPhotoTeaserAlreadyShown() {
318        getEditor().putBoolean(PreferenceKeys.CONVERSATION_PHOTO_TEASER_SHOWN, true).apply();
319    }
320
321    /**
322     * Returns whether the tip has been shown before
323     */
324    public boolean isLongPressToSelectTipAlreadyShown() {
325        // Using an int instead of boolean here in case we need to reshow the tip (don't have
326        // to use a new preference name).
327        return getSharedPreferences()
328                .getInt(PreferenceKeys.LONG_PRESS_TO_SELECT_TIP_SHOWN, 0) > 0;
329    }
330
331    public void setLongPressToSelectTipAlreadyShown() {
332        getEditor().putInt(PreferenceKeys.LONG_PRESS_TO_SELECT_TIP_SHOWN, 1).apply();
333        notifyBackupPreferenceChanged();
334    }
335
336    public void setSenderWhitelist(Set<String> addresses) {
337        getEditor().putStringSet(PreferenceKeys.DISPLAY_IMAGES, addresses).apply();
338        notifyBackupPreferenceChanged();
339    }
340    public void setSenderWhitelistPatterns(Set<String> patterns) {
341        getEditor().putStringSet(PreferenceKeys.DISPLAY_IMAGES_PATTERNS, patterns).apply();
342        notifyBackupPreferenceChanged();
343    }
344
345    /**
346     * Returns whether or not an email address is in the whitelist of senders to show images for.
347     * This method reads the entire whitelist, so if you have multiple emails to check, you should
348     * probably call getSenderWhitelist() and check membership yourself.
349     *
350     * @param sender raw email address ("foo@bar.com")
351     * @return whether we should show pictures for this sender
352     */
353    public boolean getDisplayImagesFromSender(String sender) {
354        boolean displayImages = getSenderWhitelist().contains(sender);
355        if (!displayImages) {
356            final SharedPreferences sharedPreferences = getSharedPreferences();
357            // Check the saved email address patterns to determine if this pattern matches
358            final Set<String> defaultPatternSet = Collections.emptySet();
359            final Set<String> currentPatterns = sharedPreferences.getStringSet(
360                        PreferenceKeys.DISPLAY_IMAGES_PATTERNS, defaultPatternSet);
361            for (String pattern : currentPatterns) {
362                displayImages = Pattern.compile(pattern).matcher(sender).matches();
363                if (displayImages) {
364                    break;
365                }
366            }
367        }
368
369        return displayImages;
370    }
371
372
373    public void setDisplayImagesFromSender(String sender, List<Pattern> allowedPatterns) {
374        if (allowedPatterns != null) {
375            // Look at the list of patterns where we want to allow a particular class of
376            // email address
377            for (Pattern pattern : allowedPatterns) {
378                if (pattern.matcher(sender).matches()) {
379                    // The specified email address matches one of the social network patterns.
380                    // Save the pattern itself
381                    final Set<String> currentPatterns = getSenderWhitelistPatterns();
382                    final String patternRegex = pattern.pattern();
383                    if (!currentPatterns.contains(patternRegex)) {
384                        // Copy strings to a modifiable set
385                        final Set<String> updatedPatterns = Sets.newHashSet(currentPatterns);
386                        updatedPatterns.add(patternRegex);
387                        setSenderWhitelistPatterns(updatedPatterns);
388                    }
389                    return;
390                }
391            }
392        }
393        final Set<String> whitelist = getSenderWhitelist();
394        if (!whitelist.contains(sender)) {
395            // Storing a JSONObject is slightly more nice in that maps are guaranteed to not have
396            // duplicate entries, but using a Set as intermediate representation guarantees this
397            // for us anyway. Also, using maps to represent sets forces you to pick values for
398            // them, and that's weird.
399            final Set<String> updatedList = Sets.newHashSet(whitelist);
400            updatedList.add(sender);
401            setSenderWhitelist(updatedList);
402        }
403    }
404
405    private Set<String> getSenderWhitelist() {
406        final SharedPreferences sharedPreferences = getSharedPreferences();
407        final Set<String> defaultAddressSet = Collections.emptySet();
408        return sharedPreferences.getStringSet(PreferenceKeys.DISPLAY_IMAGES, defaultAddressSet);
409    }
410
411
412    private Set<String> getSenderWhitelistPatterns() {
413        final SharedPreferences sharedPreferences = getSharedPreferences();
414        final Set<String> defaultPatternSet = Collections.emptySet();
415        return sharedPreferences.getStringSet(PreferenceKeys.DISPLAY_IMAGES_PATTERNS,
416                defaultPatternSet);
417    }
418
419    public void clearSenderWhiteList() {
420        final SharedPreferences.Editor editor = getEditor();
421        editor.putStringSet(PreferenceKeys.DISPLAY_IMAGES, Collections.EMPTY_SET);
422        editor.putStringSet(PreferenceKeys.DISPLAY_IMAGES_PATTERNS, Collections.EMPTY_SET);
423        editor.apply();
424    }
425
426    public void setShowSenderImages(boolean enable) {
427        getEditor().putBoolean(PreferenceKeys.SHOW_SENDER_IMAGES, enable).apply();
428        notifyBackupPreferenceChanged();
429    }
430
431    public boolean getShowSenderImages() {
432        final SharedPreferences sharedPreferences = getSharedPreferences();
433        return sharedPreferences.getBoolean(PreferenceKeys.SHOW_SENDER_IMAGES, true);
434    }
435
436    public int getNumOfDismissesForAutoSyncOff() {
437        return getSharedPreferences().getInt(PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, 0);
438    }
439
440    public void resetNumOfDismissesForAutoSyncOff() {
441        final int value = getSharedPreferences().getInt(
442                PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, 0);
443        if (value != 0) {
444            getEditor().putInt(PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, 0).apply();
445        }
446    }
447
448    public void incNumOfDismissesForAutoSyncOff() {
449        final int value = getSharedPreferences().getInt(
450                PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, 0);
451        getEditor().putInt(PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, value + 1).apply();
452    }
453
454    public void setConfirmDelete(final boolean confirmDelete) {
455        getEditor().putBoolean(PreferenceKeys.CONFIRM_DELETE, confirmDelete).apply();
456        notifyBackupPreferenceChanged();
457    }
458
459    public boolean getConfirmDelete() {
460        return getSharedPreferences().getBoolean(PreferenceKeys.CONFIRM_DELETE, false);
461    }
462
463    public void setConfirmArchive(final boolean confirmArchive) {
464        getEditor().putBoolean(PreferenceKeys.CONFIRM_ARCHIVE, confirmArchive).apply();
465        notifyBackupPreferenceChanged();
466    }
467
468    public boolean getConfirmArchive() {
469        return getSharedPreferences().getBoolean(PreferenceKeys.CONFIRM_ARCHIVE, false);
470    }
471
472    public void setConfirmSend(final boolean confirmSend) {
473        getEditor().putBoolean(PreferenceKeys.CONFIRM_SEND, confirmSend).apply();
474        notifyBackupPreferenceChanged();
475    }
476
477    public boolean getConfirmSend() {
478        return getSharedPreferences().getBoolean(PreferenceKeys.CONFIRM_SEND, false);
479    }
480
481    public void setAutoAdvanceMode(final int mode) {
482        getEditor().putInt(PreferenceKeys.AUTO_ADVANCE_MODE, mode).apply();
483        notifyBackupPreferenceChanged();
484    }
485
486    public int getAutoAdvanceMode() {
487        return getSharedPreferences()
488                .getInt(PreferenceKeys.AUTO_ADVANCE_MODE, UIProvider.AutoAdvance.DEFAULT);
489    }
490
491    public void setConversationOverviewMode(final boolean overviewMode) {
492        getEditor().putBoolean(PreferenceKeys.CONVERSATION_OVERVIEW_MODE, overviewMode).apply();
493    }
494
495    public boolean getConversationOverviewMode() {
496        return getSharedPreferences()
497                .getBoolean(PreferenceKeys.CONVERSATION_OVERVIEW_MODE, true);
498    }
499
500    public boolean isConversationOverviewModeSet() {
501        return getSharedPreferences().contains(PreferenceKeys.CONVERSATION_OVERVIEW_MODE);
502    }
503
504    public void setSnapHeaderMode(final int snapHeaderMode) {
505        getEditor().putInt(PreferenceKeys.SNAP_HEADER_MODE, snapHeaderMode).apply();
506    }
507
508    public int getSnapHeaderMode() {
509        return getSharedPreferences()
510                .getInt(PreferenceKeys.SNAP_HEADER_MODE, mSnapHeaderDefault);
511    }
512
513    public int getSnapHeaderDefault() {
514        return mSnapHeaderDefault;
515    }
516
517    public Set<String> getRecentAccounts() {
518        return getSharedPreferences().getStringSet(PreferenceKeys.RECENT_ACCOUNTS, null);
519    }
520
521    public void setRecentAccounts(Set<String> recentAccounts) {
522        getEditor().putStringSet(PreferenceKeys.RECENT_ACCOUNTS, recentAccounts).apply();
523    }
524}
525