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 23 import com.android.mail.providers.Account; 24 import com.android.mail.providers.UIProvider; 25 import com.android.mail.utils.LogUtils; 26 import com.android.mail.widget.BaseWidgetProvider; 27 28 import com.google.common.collect.ImmutableSet; 29 import com.google.common.collect.Sets; 30 31 import java.util.Collections; 32 import java.util.List; 33 import java.util.Set; 34 import java.util.regex.Pattern; 35 36 /** 37 * A high-level API to store and retrieve unified mail preferences. 38 * <p> 39 * This will serve as an eventual replacement for Gmail's Persistence class. 40 */ 41 public final class MailPrefs extends VersionedPrefs { 42 43 public static final boolean SHOW_EXPERIMENTAL_PREFS = false; 44 45 private static final String PREFS_NAME = "UnifiedEmail"; 46 47 private static MailPrefs sInstance; 48 49 public static final class PreferenceKeys { 50 private static final String MIGRATED_VERSION = "migrated-version"; 51 52 public static final String WIDGET_ACCOUNT_PREFIX = "widget-account-"; 53 54 /** Hidden preference to indicate what version a "What's New" dialog was last shown for. */ 55 public static final String WHATS_NEW_LAST_SHOWN_VERSION = "whats-new-last-shown-version"; 56 57 /** 58 * A boolean that, if <code>true</code>, means we should default all replies to "reply all" 59 */ 60 public static final String DEFAULT_REPLY_ALL = "default-reply-all"; 61 /** 62 * A boolean that, if <code>true</code>, means we should allow conversation list swiping 63 */ 64 public static final String CONVERSATION_LIST_SWIPE = "conversation-list-swipe"; 65 66 /** A string indicating the user's removal action preference. */ 67 public static final String REMOVAL_ACTION = "removal-action"; 68 69 /** Hidden preference used to cache the active notification set */ 70 private static final String CACHED_ACTIVE_NOTIFICATION_SET = 71 "cache-active-notification-set"; 72 73 /** 74 * A string indicating whether the conversation photo teaser has been previously 75 * shown and dismissed. This is the third version of it (thus the three at the end). 76 * Previous versions: "conversation-photo-teaser-shown" 77 * and "conversation-photo-teaser-shown-two". 78 */ 79 private static final String 80 CONVERSATION_PHOTO_TEASER_SHOWN = "conversation-photo-teaser-shown-three"; 81 82 public static final String DISPLAY_IMAGES = "display_images"; 83 public static final String DISPLAY_IMAGES_PATTERNS = "display_sender_images_patterns_set"; 84 85 86 public static final String SHOW_SENDER_IMAGES = "conversation-list-sender-image"; 87 88 public static final String 89 LONG_PRESS_TO_SELECT_TIP_SHOWN = "long-press-to-select-tip-shown"; 90 91 public static final String EXPERIMENT_AP_PARALLAX_SPEED_ALTERNATIVE = "ap-parallax-speed"; 92 public static final String EXPERIMENT_AP_PARALLAX_DIRECTION_ALTERNATIVE 93 = "ap-parallax-direction"; 94 95 public static final String GLOBAL_SYNC_OFF_DISMISSES = "num-of-dismisses-auto-sync-off"; 96 public static final String AIRPLANE_MODE_ON_DISMISSES = "num-of-dismisses-airplane-mode-on"; 97 98 public static final ImmutableSet<String> BACKUP_KEYS = 99 new ImmutableSet.Builder<String>() 100 .add(DEFAULT_REPLY_ALL) 101 .add(CONVERSATION_LIST_SWIPE) 102 .add(REMOVAL_ACTION) 103 .add(DISPLAY_IMAGES) 104 .add(DISPLAY_IMAGES_PATTERNS) 105 .add(SHOW_SENDER_IMAGES) 106 .add(LONG_PRESS_TO_SELECT_TIP_SHOWN) 107 .build(); 108 } 109 110 public static final class ConversationListSwipeActions { 111 public static final String ARCHIVE = "archive"; 112 public static final String DELETE = "delete"; 113 public static final String DISABLED = "disabled"; 114 } 115 116 public static final class RemovalActions { 117 public static final String ARCHIVE = "archive"; 118 public static final String DELETE = "delete"; 119 public static final String ARCHIVE_AND_DELETE = "archive-and-delete"; 120 } 121 122 public static MailPrefs get(Context c) { 123 if (sInstance == null) { 124 sInstance = new MailPrefs(c); 125 } 126 return sInstance; 127 } 128 129 private MailPrefs(Context c) { 130 super(c, PREFS_NAME); 131 } 132 133 @Override 134 protected void performUpgrade(final int oldVersion, final int newVersion) { 135 if (oldVersion > newVersion) { 136 throw new IllegalStateException( 137 "You appear to have downgraded your app. Please clear app data."); 138 } else if (oldVersion == newVersion) { 139 return; 140 } 141 } 142 143 @Override 144 protected boolean canBackup(final String key) { 145 return PreferenceKeys.BACKUP_KEYS.contains(key); 146 } 147 148 @Override 149 protected boolean hasMigrationCompleted() { 150 return getSharedPreferences().getInt(PreferenceKeys.MIGRATED_VERSION, 0) 151 >= CURRENT_VERSION_NUMBER; 152 } 153 154 @Override 155 protected void setMigrationComplete() { 156 getEditor().putInt(PreferenceKeys.MIGRATED_VERSION, CURRENT_VERSION_NUMBER).commit(); 157 } 158 159 public boolean isWidgetConfigured(int appWidgetId) { 160 return getSharedPreferences().contains(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + appWidgetId); 161 } 162 163 public void configureWidget(int appWidgetId, Account account, final String folderUri) { 164 if (account == null) { 165 LogUtils.e(LOG_TAG, "Cannot configure widget with null account"); 166 return; 167 } 168 getEditor().putString(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + appWidgetId, 169 createWidgetPreferenceValue(account, folderUri)).apply(); 170 } 171 172 public String getWidgetConfiguration(int appWidgetId) { 173 return getSharedPreferences().getString(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + appWidgetId, 174 null); 175 } 176 177 private static String createWidgetPreferenceValue(Account account, String folderUri) { 178 return account.uri.toString() + BaseWidgetProvider.ACCOUNT_FOLDER_PREFERENCE_SEPARATOR 179 + folderUri; 180 181 } 182 183 public void clearWidgets(int[] appWidgetIds) { 184 for (int id : appWidgetIds) { 185 getEditor().remove(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + id); 186 } 187 getEditor().apply(); 188 } 189 190 /** If <code>true</code>, we should default all replies to "reply all" rather than "reply" */ 191 public boolean getDefaultReplyAll() { 192 return getSharedPreferences().getBoolean(PreferenceKeys.DEFAULT_REPLY_ALL, false); 193 } 194 195 public void setDefaultReplyAll(final boolean replyAll) { 196 getEditor().putBoolean(PreferenceKeys.DEFAULT_REPLY_ALL, replyAll).apply(); 197 notifyBackupPreferenceChanged(); 198 } 199 200 /** 201 * Returns a string indicating the preferred removal action. 202 * Should be one of the {@link RemovalActions}. 203 */ 204 public String getRemovalAction(final boolean supportsArchive) { 205 final String defaultAction = supportsArchive 206 ? RemovalActions.ARCHIVE_AND_DELETE : RemovalActions.DELETE; 207 208 final SharedPreferences sharedPreferences = getSharedPreferences(); 209 return sharedPreferences.getString(PreferenceKeys.REMOVAL_ACTION, defaultAction); 210 } 211 212 /** 213 * Sets the removal action preference. 214 * @param removalAction The preferred {@link RemovalActions}. 215 */ 216 public void setRemovalAction(final String removalAction) { 217 getEditor().putString(PreferenceKeys.REMOVAL_ACTION, removalAction).apply(); 218 notifyBackupPreferenceChanged(); 219 } 220 221 /** 222 * Gets a boolean indicating whether conversation list swiping is enabled. 223 */ 224 public boolean getIsConversationListSwipeEnabled() { 225 final SharedPreferences sharedPreferences = getSharedPreferences(); 226 return sharedPreferences.getBoolean(PreferenceKeys.CONVERSATION_LIST_SWIPE, true); 227 } 228 229 public void setConversationListSwipeEnabled(final boolean enabled) { 230 getEditor().putBoolean(PreferenceKeys.CONVERSATION_LIST_SWIPE, enabled).apply(); 231 notifyBackupPreferenceChanged(); 232 } 233 234 /** 235 * Gets the action to take (one of the values from {@link UIProvider.Swipe}) when an item in the 236 * conversation list is swiped. 237 * 238 * @param allowArchive <code>true</code> if Archive is an acceptable action (this will affect 239 * the default return value) 240 */ 241 public int getConversationListSwipeActionInteger(final boolean allowArchive) { 242 final boolean swipeEnabled = getIsConversationListSwipeEnabled(); 243 final boolean archive = !RemovalActions.DELETE.equals(getRemovalAction(allowArchive)); 244 245 if (swipeEnabled) { 246 return archive ? UIProvider.Swipe.ARCHIVE : UIProvider.Swipe.DELETE; 247 } 248 249 return UIProvider.Swipe.DISABLED; 250 } 251 252 /** 253 * Returns the previously cached notification set 254 */ 255 public Set<String> getActiveNotificationSet() { 256 return getSharedPreferences() 257 .getStringSet(PreferenceKeys.CACHED_ACTIVE_NOTIFICATION_SET, null); 258 } 259 260 /** 261 * Caches the current notification set. 262 */ 263 public void cacheActiveNotificationSet(final Set<String> notificationSet) { 264 getEditor().putStringSet(PreferenceKeys.CACHED_ACTIVE_NOTIFICATION_SET, notificationSet) 265 .apply(); 266 } 267 268 /** 269 * Returns whether the teaser has been shown before 270 */ 271 public boolean isConversationPhotoTeaserAlreadyShown() { 272 return getSharedPreferences() 273 .getBoolean(PreferenceKeys.CONVERSATION_PHOTO_TEASER_SHOWN, false); 274 } 275 276 /** 277 * Notify that we have shown the teaser 278 */ 279 public void setConversationPhotoTeaserAlreadyShown() { 280 getEditor().putBoolean(PreferenceKeys.CONVERSATION_PHOTO_TEASER_SHOWN, true).apply(); 281 } 282 283 /** 284 * Returns whether the tip has been shown before 285 */ 286 public boolean isLongPressToSelectTipAlreadyShown() { 287 // Using an int instead of boolean here in case we need to reshow the tip (don't have 288 // to use a new preference name). 289 return getSharedPreferences() 290 .getInt(PreferenceKeys.LONG_PRESS_TO_SELECT_TIP_SHOWN, 0) > 0; 291 } 292 293 public void setLongPressToSelectTipAlreadyShown() { 294 getEditor().putInt(PreferenceKeys.LONG_PRESS_TO_SELECT_TIP_SHOWN, 1).apply(); 295 notifyBackupPreferenceChanged(); 296 } 297 298 public void setSenderWhitelist(Set<String> addresses) { 299 getEditor().putStringSet(PreferenceKeys.DISPLAY_IMAGES, addresses).apply(); 300 notifyBackupPreferenceChanged(); 301 } 302 public void setSenderWhitelistPatterns(Set<String> patterns) { 303 getEditor().putStringSet(PreferenceKeys.DISPLAY_IMAGES_PATTERNS, patterns).apply(); 304 notifyBackupPreferenceChanged(); 305 } 306 307 /** 308 * Returns whether or not an email address is in the whitelist of senders to show images for. 309 * This method reads the entire whitelist, so if you have multiple emails to check, you should 310 * probably call getSenderWhitelist() and check membership yourself. 311 * 312 * @param sender raw email address ("foo (at) bar.com") 313 * @return whether we should show pictures for this sender 314 */ 315 public boolean getDisplayImagesFromSender(String sender) { 316 boolean displayImages = getSenderWhitelist().contains(sender); 317 if (!displayImages) { 318 final SharedPreferences sharedPreferences = getSharedPreferences(); 319 // Check the saved email address patterns to determine if this pattern matches 320 final Set<String> defaultPatternSet = Collections.emptySet(); 321 final Set<String> currentPatterns = sharedPreferences.getStringSet( 322 PreferenceKeys.DISPLAY_IMAGES_PATTERNS, defaultPatternSet); 323 for (String pattern : currentPatterns) { 324 displayImages = Pattern.compile(pattern).matcher(sender).matches(); 325 if (displayImages) { 326 break; 327 } 328 } 329 } 330 331 return displayImages; 332 } 333 334 335 public void setDisplayImagesFromSender(String sender, List<Pattern> allowedPatterns) { 336 if (allowedPatterns != null) { 337 // Look at the list of patterns where we want to allow a particular class of 338 // email address 339 for (Pattern pattern : allowedPatterns) { 340 if (pattern.matcher(sender).matches()) { 341 // The specified email address matches one of the social network patterns. 342 // Save the pattern itself 343 final Set<String> currentPatterns = getSenderWhitelistPatterns(); 344 final String patternRegex = pattern.pattern(); 345 if (!currentPatterns.contains(patternRegex)) { 346 // Copy strings to a modifiable set 347 final Set<String> updatedPatterns = Sets.newHashSet(currentPatterns); 348 updatedPatterns.add(patternRegex); 349 setSenderWhitelistPatterns(updatedPatterns); 350 } 351 return; 352 } 353 } 354 } 355 final Set<String> whitelist = getSenderWhitelist(); 356 if (!whitelist.contains(sender)) { 357 // Storing a JSONObject is slightly more nice in that maps are guaranteed to not have 358 // duplicate entries, but using a Set as intermediate representation guarantees this 359 // for us anyway. Also, using maps to represent sets forces you to pick values for 360 // them, and that's weird. 361 final Set<String> updatedList = Sets.newHashSet(whitelist); 362 updatedList.add(sender); 363 setSenderWhitelist(updatedList); 364 } 365 } 366 367 private Set<String> getSenderWhitelist() { 368 final SharedPreferences sharedPreferences = getSharedPreferences(); 369 final Set<String> defaultAddressSet = Collections.emptySet(); 370 return sharedPreferences.getStringSet(PreferenceKeys.DISPLAY_IMAGES, defaultAddressSet); 371 } 372 373 374 private Set<String> getSenderWhitelistPatterns() { 375 final SharedPreferences sharedPreferences = getSharedPreferences(); 376 final Set<String> defaultPatternSet = Collections.emptySet(); 377 return sharedPreferences.getStringSet(PreferenceKeys.DISPLAY_IMAGES_PATTERNS, 378 defaultPatternSet); 379 } 380 381 public void clearSenderWhiteList() { 382 final SharedPreferences.Editor editor = getEditor(); 383 editor.putStringSet(PreferenceKeys.DISPLAY_IMAGES, Collections.EMPTY_SET); 384 editor.putStringSet(PreferenceKeys.DISPLAY_IMAGES_PATTERNS, Collections.EMPTY_SET); 385 editor.apply(); 386 } 387 388 389 public void setShowSenderImages(boolean enable) { 390 getEditor().putBoolean(PreferenceKeys.SHOW_SENDER_IMAGES, enable).apply(); 391 notifyBackupPreferenceChanged(); 392 } 393 394 public boolean getShowSenderImages() { 395 final SharedPreferences sharedPreferences = getSharedPreferences(); 396 return sharedPreferences.getBoolean(PreferenceKeys.SHOW_SENDER_IMAGES, true); 397 } 398 399 public void setParallaxSpeedAlternative(final boolean alternative) { 400 getEditor().putBoolean(PreferenceKeys.EXPERIMENT_AP_PARALLAX_SPEED_ALTERNATIVE, alternative) 401 .apply(); 402 notifyBackupPreferenceChanged(); 403 } 404 405 public boolean getParallaxSpeedAlternative() { 406 final SharedPreferences sharedPreferences = getSharedPreferences(); 407 return sharedPreferences 408 .getBoolean(PreferenceKeys.EXPERIMENT_AP_PARALLAX_SPEED_ALTERNATIVE, false); 409 } 410 411 public void setParallaxDirectionAlternative(final boolean alternative) { 412 getEditor().putBoolean(PreferenceKeys.EXPERIMENT_AP_PARALLAX_DIRECTION_ALTERNATIVE, 413 alternative).apply(); 414 notifyBackupPreferenceChanged(); 415 } 416 417 public boolean getParallaxDirectionAlternative() { 418 final SharedPreferences sharedPreferences = getSharedPreferences(); 419 return sharedPreferences 420 .getBoolean(PreferenceKeys.EXPERIMENT_AP_PARALLAX_DIRECTION_ALTERNATIVE, false); 421 } 422 423 public int getNumOfDismissesForAutoSyncOff() { 424 return getSharedPreferences().getInt(PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, 0); 425 } 426 427 public void resetNumOfDismissesForAutoSyncOff() { 428 final int value = getSharedPreferences().getInt( 429 PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, 0); 430 if (value != 0) { 431 getEditor().putInt(PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, 0).apply(); 432 } 433 } 434 435 public void incNumOfDismissesForAutoSyncOff() { 436 final int value = getSharedPreferences().getInt( 437 PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, 0); 438 getEditor().putInt(PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, value + 1).apply(); 439 } 440 } 441