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