• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
18 package com.android.mail.preferences;
19 
20 import android.content.Context;
21 import android.content.SharedPreferences;
22 import android.support.annotation.StringDef;
23 
24 import com.android.mail.R;
25 import com.android.mail.providers.Account;
26 import com.android.mail.providers.UIProvider;
27 import com.android.mail.utils.LogUtils;
28 import com.android.mail.widget.BaseWidgetProvider;
29 import com.google.common.annotations.VisibleForTesting;
30 import com.google.common.collect.ImmutableSet;
31 import com.google.common.collect.Sets;
32 
33 import java.lang.annotation.Retention;
34 import java.lang.annotation.RetentionPolicy;
35 import java.util.Collections;
36 import java.util.List;
37 import java.util.Set;
38 import 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  */
45 public 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 
get(final Context c)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
MailPrefs(final Context c, final String prefsName)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
performUpgrade(final int oldVersion, final int newVersion)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
canBackup(final String key)180     protected boolean canBackup(final String key) {
181         return PreferenceKeys.BACKUP_KEYS.contains(key);
182     }
183 
184     @Override
hasMigrationCompleted()185     protected boolean hasMigrationCompleted() {
186         return getSharedPreferences().getInt(PreferenceKeys.MIGRATED_VERSION, 0)
187                 >= CURRENT_VERSION_NUMBER;
188     }
189 
190     @Override
setMigrationComplete()191     protected void setMigrationComplete() {
192         getEditor().putInt(PreferenceKeys.MIGRATED_VERSION, CURRENT_VERSION_NUMBER).commit();
193     }
194 
isWidgetConfigured(int appWidgetId)195     public boolean isWidgetConfigured(int appWidgetId) {
196         return getSharedPreferences().contains(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + appWidgetId);
197     }
198 
configureWidget(int appWidgetId, Account account, final String folderUri)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 
getWidgetConfiguration(int appWidgetId)208     public String getWidgetConfiguration(int appWidgetId) {
209         return getSharedPreferences().getString(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + appWidgetId,
210                 null);
211     }
212 
createWidgetPreferenceValue(Account account, String folderUri)213     private static String createWidgetPreferenceValue(Account account, String folderUri) {
214         return account.uri.toString() + BaseWidgetProvider.ACCOUNT_FOLDER_PREFERENCE_SEPARATOR
215                 + folderUri;
216 
217     }
218 
clearWidgets(int[] appWidgetIds)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" */
getDefaultReplyAll()227     public boolean getDefaultReplyAll() {
228         return getSharedPreferences().getBoolean(PreferenceKeys.DEFAULT_REPLY_ALL, false);
229     }
230 
setDefaultReplyAll(final boolean replyAll)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      */
getRemovalAction(final boolean supportsArchive)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      */
setRemovalAction(final @RemovalActionTypes String removalAction)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      */
getIsConversationListSwipeEnabled()262     public boolean getIsConversationListSwipeEnabled() {
263         final SharedPreferences sharedPreferences = getSharedPreferences();
264         return sharedPreferences.getBoolean(PreferenceKeys.CONVERSATION_LIST_SWIPE, true);
265     }
266 
setConversationListSwipeEnabled(final boolean enabled)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      */
getConversationListSwipeActionInteger(final boolean allowArchive)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      */
getActiveNotificationSet()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      */
cacheActiveNotificationSet(final Set<String> notificationSet)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      */
isConversationPhotoTeaserAlreadyShown()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      */
setConversationPhotoTeaserAlreadyShown()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      */
isLongPressToSelectTipAlreadyShown()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 
setLongPressToSelectTipAlreadyShown()331     public void setLongPressToSelectTipAlreadyShown() {
332         getEditor().putInt(PreferenceKeys.LONG_PRESS_TO_SELECT_TIP_SHOWN, 1).apply();
333         notifyBackupPreferenceChanged();
334     }
335 
setSenderWhitelist(Set<String> addresses)336     public void setSenderWhitelist(Set<String> addresses) {
337         getEditor().putStringSet(PreferenceKeys.DISPLAY_IMAGES, addresses).apply();
338         notifyBackupPreferenceChanged();
339     }
setSenderWhitelistPatterns(Set<String> patterns)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      */
getDisplayImagesFromSender(String sender)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 
setDisplayImagesFromSender(String sender, List<Pattern> allowedPatterns)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 
getSenderWhitelist()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 
getSenderWhitelistPatterns()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 
clearSenderWhiteList()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 
setShowSenderImages(boolean enable)426     public void setShowSenderImages(boolean enable) {
427         getEditor().putBoolean(PreferenceKeys.SHOW_SENDER_IMAGES, enable).apply();
428         notifyBackupPreferenceChanged();
429     }
430 
getShowSenderImages()431     public boolean getShowSenderImages() {
432         final SharedPreferences sharedPreferences = getSharedPreferences();
433         return sharedPreferences.getBoolean(PreferenceKeys.SHOW_SENDER_IMAGES, true);
434     }
435 
getNumOfDismissesForAutoSyncOff()436     public int getNumOfDismissesForAutoSyncOff() {
437         return getSharedPreferences().getInt(PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, 0);
438     }
439 
resetNumOfDismissesForAutoSyncOff()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 
incNumOfDismissesForAutoSyncOff()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 
setConfirmDelete(final boolean confirmDelete)454     public void setConfirmDelete(final boolean confirmDelete) {
455         getEditor().putBoolean(PreferenceKeys.CONFIRM_DELETE, confirmDelete).apply();
456         notifyBackupPreferenceChanged();
457     }
458 
getConfirmDelete()459     public boolean getConfirmDelete() {
460         return getSharedPreferences().getBoolean(PreferenceKeys.CONFIRM_DELETE, false);
461     }
462 
setConfirmArchive(final boolean confirmArchive)463     public void setConfirmArchive(final boolean confirmArchive) {
464         getEditor().putBoolean(PreferenceKeys.CONFIRM_ARCHIVE, confirmArchive).apply();
465         notifyBackupPreferenceChanged();
466     }
467 
getConfirmArchive()468     public boolean getConfirmArchive() {
469         return getSharedPreferences().getBoolean(PreferenceKeys.CONFIRM_ARCHIVE, false);
470     }
471 
setConfirmSend(final boolean confirmSend)472     public void setConfirmSend(final boolean confirmSend) {
473         getEditor().putBoolean(PreferenceKeys.CONFIRM_SEND, confirmSend).apply();
474         notifyBackupPreferenceChanged();
475     }
476 
getConfirmSend()477     public boolean getConfirmSend() {
478         return getSharedPreferences().getBoolean(PreferenceKeys.CONFIRM_SEND, false);
479     }
480 
setAutoAdvanceMode(final int mode)481     public void setAutoAdvanceMode(final int mode) {
482         getEditor().putInt(PreferenceKeys.AUTO_ADVANCE_MODE, mode).apply();
483         notifyBackupPreferenceChanged();
484     }
485 
getAutoAdvanceMode()486     public int getAutoAdvanceMode() {
487         return getSharedPreferences()
488                 .getInt(PreferenceKeys.AUTO_ADVANCE_MODE, UIProvider.AutoAdvance.DEFAULT);
489     }
490 
setConversationOverviewMode(final boolean overviewMode)491     public void setConversationOverviewMode(final boolean overviewMode) {
492         getEditor().putBoolean(PreferenceKeys.CONVERSATION_OVERVIEW_MODE, overviewMode).apply();
493     }
494 
getConversationOverviewMode()495     public boolean getConversationOverviewMode() {
496         return getSharedPreferences()
497                 .getBoolean(PreferenceKeys.CONVERSATION_OVERVIEW_MODE, true);
498     }
499 
isConversationOverviewModeSet()500     public boolean isConversationOverviewModeSet() {
501         return getSharedPreferences().contains(PreferenceKeys.CONVERSATION_OVERVIEW_MODE);
502     }
503 
setSnapHeaderMode(final int snapHeaderMode)504     public void setSnapHeaderMode(final int snapHeaderMode) {
505         getEditor().putInt(PreferenceKeys.SNAP_HEADER_MODE, snapHeaderMode).apply();
506     }
507 
getSnapHeaderMode()508     public int getSnapHeaderMode() {
509         return getSharedPreferences()
510                 .getInt(PreferenceKeys.SNAP_HEADER_MODE, mSnapHeaderDefault);
511     }
512 
getSnapHeaderDefault()513     public int getSnapHeaderDefault() {
514         return mSnapHeaderDefault;
515     }
516 
getRecentAccounts()517     public Set<String> getRecentAccounts() {
518         return getSharedPreferences().getStringSet(PreferenceKeys.RECENT_ACCOUNTS, null);
519     }
520 
setRecentAccounts(Set<String> recentAccounts)521     public void setRecentAccounts(Set<String> recentAccounts) {
522         getEditor().putStringSet(PreferenceKeys.RECENT_ACCOUNTS, recentAccounts).apply();
523     }
524 }
525