/* * Copyright (C) 2012 Google Inc. * Licensed to The Android Open Source Project. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.mail.preferences; import android.content.Context; import android.content.SharedPreferences; import android.support.annotation.StringDef; import android.text.TextUtils; import com.android.mail.R; import com.android.mail.providers.Account; import com.android.mail.providers.UIProvider; import com.android.mail.utils.LogUtils; import com.android.mail.widget.BaseWidgetProvider; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.regex.Pattern; /** * A high-level API to store and retrieve unified mail preferences. *

* This will serve as an eventual replacement for Gmail's Persistence class. */ public final class MailPrefs extends VersionedPrefs { public static final boolean SHOW_EXPERIMENTAL_PREFS = false; private static final String PREFS_NAME = "UnifiedEmail"; private static MailPrefs sInstance; private final int mSnapHeaderDefault; public static final class PreferenceKeys { private static final String MIGRATED_VERSION = "migrated-version"; public static final String WIDGET_ACCOUNT_PREFIX = "widget-account-"; /** Hidden preference to indicate what version a "What's New" dialog was last shown for. */ public static final String WHATS_NEW_LAST_SHOWN_VERSION = "whats-new-last-shown-version"; /** * A boolean that, if true, means we should default all replies to "reply all" */ public static final String DEFAULT_REPLY_ALL = "default-reply-all"; /** * A boolean that, if true, means we should allow conversation list swiping */ public static final String CONVERSATION_LIST_SWIPE = "conversation-list-swipe"; /** A string indicating the user's removal action preference. */ public static final String REMOVAL_ACTION = "removal-action"; /** Hidden preference used to cache the active notification set */ private static final String CACHED_ACTIVE_NOTIFICATION_SET = "cache-active-notification-set"; /** * A string indicating whether the conversation photo teaser has been previously * shown and dismissed. This is the third version of it (thus the three at the end). * Previous versions: "conversation-photo-teaser-shown" * and "conversation-photo-teaser-shown-two". */ private static final String CONVERSATION_PHOTO_TEASER_SHOWN = "conversation-photo-teaser-shown-three"; public static final String DISPLAY_IMAGES = "display_images"; public static final String DISPLAY_IMAGES_PATTERNS = "display_sender_images_patterns_set"; public static final String SHOW_SENDER_IMAGES = "conversation-list-sender-image"; public static final String LONG_PRESS_TO_SELECT_TIP_SHOWN = "long-press-to-select-tip-shown"; /** @deprecated attachment previews have been removed; avoid future key name conflicts */ public static final String EXPERIMENT_AP_PARALLAX_SPEED_ALTERNATIVE = "ap-parallax-speed"; /** @deprecated attachment previews have been removed; avoid future key name conflicts */ public static final String EXPERIMENT_AP_PARALLAX_DIRECTION_ALTERNATIVE = "ap-parallax-direction"; public static final String GLOBAL_SYNC_OFF_DISMISSES = "num-of-dismisses-auto-sync-off"; public static final String AIRPLANE_MODE_ON_DISMISSES = "num-of-dismisses-airplane-mode-on"; public static final String AUTO_ADVANCE_MODE = "auto-advance-mode"; public static final String CONFIRM_DELETE = "confirm-delete"; public static final String CONFIRM_ARCHIVE = "confirm-archive"; public static final String CONFIRM_SEND = "confirm-send"; public static final String CONVERSATION_OVERVIEW_MODE = "conversation-overview-mode"; public static final String ALWAYS_LAUNCH_GMAIL_FROM_EMAIL_TOMBSTONE = "always-launch-gmail-from-email-tombstone"; public static final String SNAP_HEADER_MODE = "snap-header-mode"; public static final String RECENT_ACCOUNTS = "recent-accounts"; public static final String REQUIRED_SANITIZER_VERSION_NUMBER = "required-sanitizer-version-number"; public static final String MIGRATION_STATE = "migration-state"; /** * The time in epoch ms when the number of accounts in the app was reported to analytics. */ public static final String ANALYTICS_NB_ACCOUNT_LATEST_REPORT = "analytics-send-nb_accounts-epoch"; // State indicating that no migration has yet occurred. public static final int MIGRATION_STATE_NONE = 0; // State indicating that we have migrated imap and pop accounts, but not // Exchange accounts. public static final int MIGRATION_STATE_IMAP_POP = 1; // State indicating that we have migrated all accounts. public static final int MIGRATION_STATE_ALL = 2; public static final ImmutableSet BACKUP_KEYS = new ImmutableSet.Builder() .add(DEFAULT_REPLY_ALL) .add(CONVERSATION_LIST_SWIPE) .add(REMOVAL_ACTION) .add(DISPLAY_IMAGES) .add(DISPLAY_IMAGES_PATTERNS) .add(SHOW_SENDER_IMAGES) .add(LONG_PRESS_TO_SELECT_TIP_SHOWN) .add(AUTO_ADVANCE_MODE) .add(CONFIRM_DELETE) .add(CONFIRM_ARCHIVE) .add(CONFIRM_SEND) .add(CONVERSATION_OVERVIEW_MODE) .add(SNAP_HEADER_MODE) .build(); } public static final class ConversationListSwipeActions { public static final String ARCHIVE = "archive"; public static final String DELETE = "delete"; public static final String DISABLED = "disabled"; } @Retention(RetentionPolicy.SOURCE) @StringDef({ RemovalActions.ARCHIVE, RemovalActions.DELETE }) public @interface RemovalActionTypes {} public static final class RemovalActions { public static final String ARCHIVE = "archive"; public static final String DELETE = "delete"; @Deprecated public static final String ARCHIVE_AND_DELETE = "archive-and-delete"; } public static synchronized MailPrefs get(final Context c) { if (sInstance == null) { sInstance = new MailPrefs(c, PREFS_NAME); } return sInstance; } @VisibleForTesting public MailPrefs(final Context c, final String prefsName) { super(c, prefsName); mSnapHeaderDefault = c.getResources().getInteger(R.integer.prefDefault_snapHeader); } @Override protected void performUpgrade(final int oldVersion, final int newVersion) { if (oldVersion > newVersion) { throw new IllegalStateException( "You appear to have downgraded your app. Please clear app data."); } else if (oldVersion == newVersion) { return; } } @Override protected boolean canBackup(final String key) { return PreferenceKeys.BACKUP_KEYS.contains(key); } @Override protected boolean hasMigrationCompleted() { return getSharedPreferences().getInt(PreferenceKeys.MIGRATED_VERSION, 0) >= CURRENT_VERSION_NUMBER; } @Override protected void setMigrationComplete() { getEditor().putInt(PreferenceKeys.MIGRATED_VERSION, CURRENT_VERSION_NUMBER).commit(); } public boolean isWidgetConfigured(int appWidgetId) { return getSharedPreferences().contains(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + appWidgetId); } public void configureWidget(int appWidgetId, Account account, final String folderUri) { if (account == null) { LogUtils.e(LOG_TAG, "Cannot configure widget with null account"); return; } getEditor().putString(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + appWidgetId, createWidgetPreferenceValue(account, folderUri)).apply(); } public String getWidgetConfiguration(int appWidgetId) { return getSharedPreferences().getString(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + appWidgetId, null); } private static String createWidgetPreferenceValue(Account account, String folderUri) { return account.uri.toString() + BaseWidgetProvider.ACCOUNT_FOLDER_PREFERENCE_SEPARATOR + folderUri; } public void clearWidgets(int[] appWidgetIds) { for (int id : appWidgetIds) { getEditor().remove(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + id); } getEditor().apply(); } /** If true, we should default all replies to "reply all" rather than "reply" */ public boolean getDefaultReplyAll() { return getSharedPreferences().getBoolean(PreferenceKeys.DEFAULT_REPLY_ALL, false); } public void setDefaultReplyAll(final boolean replyAll) { getEditor().putBoolean(PreferenceKeys.DEFAULT_REPLY_ALL, replyAll).apply(); notifyBackupPreferenceChanged(); } /** * Returns a string indicating the preferred removal action. * Should be one of the {@link RemovalActions}. */ public String getRemovalAction(final boolean supportsArchive) { if (!supportsArchive) { return RemovalActions.DELETE; } final SharedPreferences sharedPreferences = getSharedPreferences(); final String removalAction = sharedPreferences.getString(PreferenceKeys.REMOVAL_ACTION, null); if (TextUtils.equals(removalAction, RemovalActions.ARCHIVE_AND_DELETE)) { return RemovalActions.ARCHIVE; } return sharedPreferences.getString(PreferenceKeys.REMOVAL_ACTION, RemovalActions.ARCHIVE); } /** * Sets the removal action preference. * @param removalAction The preferred {@link RemovalActions}. */ public void setRemovalAction(final @RemovalActionTypes String removalAction) { getEditor().putString(PreferenceKeys.REMOVAL_ACTION, removalAction).apply(); notifyBackupPreferenceChanged(); } /** * Gets a boolean indicating whether conversation list swiping is enabled. */ public boolean getIsConversationListSwipeEnabled() { final SharedPreferences sharedPreferences = getSharedPreferences(); return sharedPreferences.getBoolean(PreferenceKeys.CONVERSATION_LIST_SWIPE, true); } public void setConversationListSwipeEnabled(final boolean enabled) { getEditor().putBoolean(PreferenceKeys.CONVERSATION_LIST_SWIPE, enabled).apply(); notifyBackupPreferenceChanged(); } /** * Gets the action to take (one of the values from {@link UIProvider.Swipe}) when an item in the * conversation list is swiped. * * @param allowArchive true if Archive is an acceptable action (this will affect * the default return value) */ public int getConversationListSwipeActionInteger(final boolean allowArchive) { final boolean swipeEnabled = getIsConversationListSwipeEnabled(); final boolean archive = !RemovalActions.DELETE.equals(getRemovalAction(allowArchive)); if (swipeEnabled) { return archive ? UIProvider.Swipe.ARCHIVE : UIProvider.Swipe.DELETE; } return UIProvider.Swipe.DISABLED; } /** * Returns the previously cached notification set */ public Set getActiveNotificationSet() { return getSharedPreferences() .getStringSet(PreferenceKeys.CACHED_ACTIVE_NOTIFICATION_SET, null); } /** * Caches the current notification set. */ public void cacheActiveNotificationSet(final Set notificationSet) { getEditor().putStringSet(PreferenceKeys.CACHED_ACTIVE_NOTIFICATION_SET, notificationSet) .apply(); } /** * Returns whether the teaser has been shown before */ public boolean isConversationPhotoTeaserAlreadyShown() { return getSharedPreferences() .getBoolean(PreferenceKeys.CONVERSATION_PHOTO_TEASER_SHOWN, false); } /** * Notify that we have shown the teaser */ public void setConversationPhotoTeaserAlreadyShown() { getEditor().putBoolean(PreferenceKeys.CONVERSATION_PHOTO_TEASER_SHOWN, true).apply(); } /** * Returns whether the tip has been shown before */ public boolean isLongPressToSelectTipAlreadyShown() { // Using an int instead of boolean here in case we need to reshow the tip (don't have // to use a new preference name). return getSharedPreferences() .getInt(PreferenceKeys.LONG_PRESS_TO_SELECT_TIP_SHOWN, 0) > 0; } public void setLongPressToSelectTipAlreadyShown() { getEditor().putInt(PreferenceKeys.LONG_PRESS_TO_SELECT_TIP_SHOWN, 1).apply(); notifyBackupPreferenceChanged(); } public void setSenderWhitelist(Set addresses) { getEditor().putStringSet(PreferenceKeys.DISPLAY_IMAGES, addresses).apply(); notifyBackupPreferenceChanged(); } public void setSenderWhitelistPatterns(Set patterns) { getEditor().putStringSet(PreferenceKeys.DISPLAY_IMAGES_PATTERNS, patterns).apply(); notifyBackupPreferenceChanged(); } /** * Returns whether or not an email address is in the whitelist of senders to show images for. * This method reads the entire whitelist, so if you have multiple emails to check, you should * probably call getSenderWhitelist() and check membership yourself. * * @param sender raw email address ("foo@bar.com") * @return whether we should show pictures for this sender */ public boolean getDisplayImagesFromSender(String sender) { boolean displayImages = getSenderWhitelist().contains(sender); if (!displayImages) { final SharedPreferences sharedPreferences = getSharedPreferences(); // Check the saved email address patterns to determine if this pattern matches final Set defaultPatternSet = Collections.emptySet(); final Set currentPatterns = sharedPreferences.getStringSet( PreferenceKeys.DISPLAY_IMAGES_PATTERNS, defaultPatternSet); for (String pattern : currentPatterns) { displayImages = Pattern.compile(pattern).matcher(sender).matches(); if (displayImages) { break; } } } return displayImages; } public void setDisplayImagesFromSender(String sender, List allowedPatterns) { if (allowedPatterns != null) { // Look at the list of patterns where we want to allow a particular class of // email address for (Pattern pattern : allowedPatterns) { if (pattern.matcher(sender).matches()) { // The specified email address matches one of the social network patterns. // Save the pattern itself final Set currentPatterns = getSenderWhitelistPatterns(); final String patternRegex = pattern.pattern(); if (!currentPatterns.contains(patternRegex)) { // Copy strings to a modifiable set final Set updatedPatterns = Sets.newHashSet(currentPatterns); updatedPatterns.add(patternRegex); setSenderWhitelistPatterns(updatedPatterns); } return; } } } final Set whitelist = getSenderWhitelist(); if (!whitelist.contains(sender)) { // Storing a JSONObject is slightly more nice in that maps are guaranteed to not have // duplicate entries, but using a Set as intermediate representation guarantees this // for us anyway. Also, using maps to represent sets forces you to pick values for // them, and that's weird. final Set updatedList = Sets.newHashSet(whitelist); updatedList.add(sender); setSenderWhitelist(updatedList); } } private Set getSenderWhitelist() { final SharedPreferences sharedPreferences = getSharedPreferences(); final Set defaultAddressSet = Collections.emptySet(); return sharedPreferences.getStringSet(PreferenceKeys.DISPLAY_IMAGES, defaultAddressSet); } private Set getSenderWhitelistPatterns() { final SharedPreferences sharedPreferences = getSharedPreferences(); final Set defaultPatternSet = Collections.emptySet(); return sharedPreferences.getStringSet(PreferenceKeys.DISPLAY_IMAGES_PATTERNS, defaultPatternSet); } public void clearSenderWhiteList() { final SharedPreferences.Editor editor = getEditor(); editor.putStringSet(PreferenceKeys.DISPLAY_IMAGES, Collections.EMPTY_SET); editor.putStringSet(PreferenceKeys.DISPLAY_IMAGES_PATTERNS, Collections.EMPTY_SET); editor.apply(); } public void setShowSenderImages(boolean enable) { getEditor().putBoolean(PreferenceKeys.SHOW_SENDER_IMAGES, enable).apply(); notifyBackupPreferenceChanged(); } public boolean getShowSenderImages() { final SharedPreferences sharedPreferences = getSharedPreferences(); return sharedPreferences.getBoolean(PreferenceKeys.SHOW_SENDER_IMAGES, true); } public int getNumOfDismissesForAutoSyncOff() { return getSharedPreferences().getInt(PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, 0); } public void resetNumOfDismissesForAutoSyncOff() { final int value = getSharedPreferences().getInt( PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, 0); if (value != 0) { getEditor().putInt(PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, 0).apply(); } } public void incNumOfDismissesForAutoSyncOff() { final int value = getSharedPreferences().getInt( PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, 0); getEditor().putInt(PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, value + 1).apply(); } public void setConfirmDelete(final boolean confirmDelete) { getEditor().putBoolean(PreferenceKeys.CONFIRM_DELETE, confirmDelete).apply(); notifyBackupPreferenceChanged(); } public boolean getConfirmDelete() { return getSharedPreferences().getBoolean(PreferenceKeys.CONFIRM_DELETE, false); } public void setConfirmArchive(final boolean confirmArchive) { getEditor().putBoolean(PreferenceKeys.CONFIRM_ARCHIVE, confirmArchive).apply(); notifyBackupPreferenceChanged(); } public boolean getConfirmArchive() { return getSharedPreferences().getBoolean(PreferenceKeys.CONFIRM_ARCHIVE, false); } public void setConfirmSend(final boolean confirmSend) { getEditor().putBoolean(PreferenceKeys.CONFIRM_SEND, confirmSend).apply(); notifyBackupPreferenceChanged(); } public boolean getConfirmSend() { return getSharedPreferences().getBoolean(PreferenceKeys.CONFIRM_SEND, false); } public void setAutoAdvanceMode(final int mode) { getEditor().putInt(PreferenceKeys.AUTO_ADVANCE_MODE, mode).apply(); notifyBackupPreferenceChanged(); } public int getAutoAdvanceMode() { return getSharedPreferences() .getInt(PreferenceKeys.AUTO_ADVANCE_MODE, UIProvider.AutoAdvance.DEFAULT); } public void setConversationOverviewMode(final boolean overviewMode) { getEditor().putBoolean(PreferenceKeys.CONVERSATION_OVERVIEW_MODE, overviewMode).apply(); } public boolean getConversationOverviewMode() { return getSharedPreferences() .getBoolean(PreferenceKeys.CONVERSATION_OVERVIEW_MODE, true); } public boolean isConversationOverviewModeSet() { return getSharedPreferences().contains(PreferenceKeys.CONVERSATION_OVERVIEW_MODE); } public void setAlwaysLaunchGmailFromEmailTombstone(final boolean alwaysLaunchGmail) { getEditor() .putBoolean(PreferenceKeys.ALWAYS_LAUNCH_GMAIL_FROM_EMAIL_TOMBSTONE, alwaysLaunchGmail) .apply(); } public boolean getAlwaysLaunchGmailFromEmailTombstone() { return getSharedPreferences() .getBoolean(PreferenceKeys.ALWAYS_LAUNCH_GMAIL_FROM_EMAIL_TOMBSTONE, false); } public void setSnapHeaderMode(final int snapHeaderMode) { getEditor().putInt(PreferenceKeys.SNAP_HEADER_MODE, snapHeaderMode).apply(); } public int getSnapHeaderMode() { return getSharedPreferences() .getInt(PreferenceKeys.SNAP_HEADER_MODE, mSnapHeaderDefault); } public int getSnapHeaderDefault() { return mSnapHeaderDefault; } public int getMigrationState() { return getSharedPreferences() .getInt(PreferenceKeys.MIGRATION_STATE, PreferenceKeys.MIGRATION_STATE_NONE); } public void setMigrationState(final int state) { getEditor().putInt(PreferenceKeys.MIGRATION_STATE, state).apply(); } public Set getRecentAccounts() { return getSharedPreferences().getStringSet(PreferenceKeys.RECENT_ACCOUNTS, null); } public void setRecentAccounts(Set recentAccounts) { getEditor().putStringSet(PreferenceKeys.RECENT_ACCOUNTS, recentAccounts).apply(); } /** * Returns the minimum version number of the {@link com.android.mail.utils.HtmlSanitizer} which * is trusted. If the version of the HtmlSanitizer does not meet or exceed this value, * sanitization will be deemed untrustworthy and emails will be displayed in a sandbox that does * not allow script execution. */ public int getRequiredSanitizerVersionNumber() { return getSharedPreferences().getInt(PreferenceKeys.REQUIRED_SANITIZER_VERSION_NUMBER, 1); } /** * @param versionNumber the minimum version number of the * {@link com.android.mail.utils.HtmlSanitizer} which produces trusted output */ public void setRequiredSanitizerVersionNumber(int versionNumber) { getEditor().putInt(PreferenceKeys.REQUIRED_SANITIZER_VERSION_NUMBER, versionNumber).apply(); } /** * Returns the latest time the number of accounts in the application was sent to Analyitcs. * @return the datetime in epoch milliseconds. */ public long getNbAccountsLatestReport() { return getSharedPreferences().getLong(PreferenceKeys.ANALYTICS_NB_ACCOUNT_LATEST_REPORT, 0); } /** * Set the latest time the number of accounts in the application was sent to Analytics. */ public void setNbAccountsLatestReport(long timeMs) { getEditor().putLong( PreferenceKeys.ANALYTICS_NB_ACCOUNT_LATEST_REPORT, timeMs); } }