Home | History | Annotate | Download | only in preferences
      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 
    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 (at) 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