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 import android.text.TextUtils; 24 25 import com.android.mail.R; 26 import com.android.mail.providers.Account; 27 import com.android.mail.providers.UIProvider; 28 import com.android.mail.utils.LogUtils; 29 import com.android.mail.widget.BaseWidgetProvider; 30 import com.google.common.annotations.VisibleForTesting; 31 import com.google.common.collect.ImmutableSet; 32 import com.google.common.collect.Sets; 33 34 import java.lang.annotation.Retention; 35 import java.lang.annotation.RetentionPolicy; 36 import java.util.Collections; 37 import java.util.List; 38 import java.util.Set; 39 import java.util.regex.Pattern; 40 41 /** 42 * A high-level API to store and retrieve unified mail preferences. 43 * <p> 44 * This will serve as an eventual replacement for Gmail's Persistence class. 45 */ 46 public final class MailPrefs extends VersionedPrefs { 47 48 public static final boolean SHOW_EXPERIMENTAL_PREFS = false; 49 50 private static final String PREFS_NAME = "UnifiedEmail"; 51 52 private static MailPrefs sInstance; 53 54 private final int mSnapHeaderDefault; 55 56 public static final class PreferenceKeys { 57 private static final String MIGRATED_VERSION = "migrated-version"; 58 59 public static final String WIDGET_ACCOUNT_PREFIX = "widget-account-"; 60 61 /** Hidden preference to indicate what version a "What's New" dialog was last shown for. */ 62 public static final String WHATS_NEW_LAST_SHOWN_VERSION = "whats-new-last-shown-version"; 63 64 /** 65 * A boolean that, if <code>true</code>, means we should default all replies to "reply all" 66 */ 67 public static final String DEFAULT_REPLY_ALL = "default-reply-all"; 68 /** 69 * A boolean that, if <code>true</code>, means we should allow conversation list swiping 70 */ 71 public static final String CONVERSATION_LIST_SWIPE = "conversation-list-swipe"; 72 73 /** A string indicating the user's removal action preference. */ 74 public static final String REMOVAL_ACTION = "removal-action"; 75 76 /** Hidden preference used to cache the active notification set */ 77 private static final String CACHED_ACTIVE_NOTIFICATION_SET = 78 "cache-active-notification-set"; 79 80 /** 81 * A string indicating whether the conversation photo teaser has been previously 82 * shown and dismissed. This is the third version of it (thus the three at the end). 83 * Previous versions: "conversation-photo-teaser-shown" 84 * and "conversation-photo-teaser-shown-two". 85 */ 86 private static final String 87 CONVERSATION_PHOTO_TEASER_SHOWN = "conversation-photo-teaser-shown-three"; 88 89 public static final String DISPLAY_IMAGES = "display_images"; 90 public static final String DISPLAY_IMAGES_PATTERNS = "display_sender_images_patterns_set"; 91 92 93 public static final String SHOW_SENDER_IMAGES = "conversation-list-sender-image"; 94 95 public static final String 96 LONG_PRESS_TO_SELECT_TIP_SHOWN = "long-press-to-select-tip-shown"; 97 98 /** @deprecated attachment previews have been removed; avoid future key name conflicts */ 99 public static final String EXPERIMENT_AP_PARALLAX_SPEED_ALTERNATIVE = "ap-parallax-speed"; 100 101 /** @deprecated attachment previews have been removed; avoid future key name conflicts */ 102 public static final String EXPERIMENT_AP_PARALLAX_DIRECTION_ALTERNATIVE 103 = "ap-parallax-direction"; 104 105 public static final String GLOBAL_SYNC_OFF_DISMISSES = "num-of-dismisses-auto-sync-off"; 106 public static final String AIRPLANE_MODE_ON_DISMISSES = "num-of-dismisses-airplane-mode-on"; 107 108 public static final String AUTO_ADVANCE_MODE = "auto-advance-mode"; 109 110 public static final String CONFIRM_DELETE = "confirm-delete"; 111 public static final String CONFIRM_ARCHIVE = "confirm-archive"; 112 public static final String CONFIRM_SEND = "confirm-send"; 113 114 public static final String CONVERSATION_OVERVIEW_MODE = "conversation-overview-mode"; 115 116 public static final String ALWAYS_LAUNCH_GMAIL_FROM_EMAIL_TOMBSTONE = 117 "always-launch-gmail-from-email-tombstone"; 118 119 public static final String SNAP_HEADER_MODE = "snap-header-mode"; 120 121 public static final String RECENT_ACCOUNTS = "recent-accounts"; 122 123 public static final String REQUIRED_SANITIZER_VERSION_NUMBER = 124 "required-sanitizer-version-number"; 125 126 public static final String MIGRATION_STATE = "migration-state"; 127 128 /** 129 * The time in epoch ms when the number of accounts in the app was reported to analytics. 130 */ 131 public static final String ANALYTICS_NB_ACCOUNT_LATEST_REPORT = 132 "analytics-send-nb_accounts-epoch"; 133 134 // State indicating that no migration has yet occurred. 135 public static final int MIGRATION_STATE_NONE = 0; 136 // State indicating that we have migrated imap and pop accounts, but not 137 // Exchange accounts. 138 public static final int MIGRATION_STATE_IMAP_POP = 1; 139 // State indicating that we have migrated all accounts. 140 public static final int MIGRATION_STATE_ALL = 2; 141 142 public static final ImmutableSet<String> BACKUP_KEYS = 143 new ImmutableSet.Builder<String>() 144 .add(DEFAULT_REPLY_ALL) 145 .add(CONVERSATION_LIST_SWIPE) 146 .add(REMOVAL_ACTION) 147 .add(DISPLAY_IMAGES) 148 .add(DISPLAY_IMAGES_PATTERNS) 149 .add(SHOW_SENDER_IMAGES) 150 .add(LONG_PRESS_TO_SELECT_TIP_SHOWN) 151 .add(AUTO_ADVANCE_MODE) 152 .add(CONFIRM_DELETE) 153 .add(CONFIRM_ARCHIVE) 154 .add(CONFIRM_SEND) 155 .add(CONVERSATION_OVERVIEW_MODE) 156 .add(SNAP_HEADER_MODE) 157 .build(); 158 } 159 160 public static final class ConversationListSwipeActions { 161 public static final String ARCHIVE = "archive"; 162 public static final String DELETE = "delete"; 163 public static final String DISABLED = "disabled"; 164 } 165 166 @Retention(RetentionPolicy.SOURCE) 167 @StringDef({ 168 RemovalActions.ARCHIVE, 169 RemovalActions.DELETE 170 }) 171 public @interface RemovalActionTypes {} 172 public static final class RemovalActions { 173 public static final String ARCHIVE = "archive"; 174 public static final String DELETE = "delete"; 175 @Deprecated 176 public static final String ARCHIVE_AND_DELETE = "archive-and-delete"; 177 } 178 179 public static synchronized MailPrefs get(final Context c) { 180 if (sInstance == null) { 181 sInstance = new MailPrefs(c, PREFS_NAME); 182 } 183 return sInstance; 184 } 185 186 @VisibleForTesting 187 public MailPrefs(final Context c, final String prefsName) { 188 super(c, prefsName); 189 mSnapHeaderDefault = c.getResources().getInteger(R.integer.prefDefault_snapHeader); 190 } 191 192 @Override 193 protected void performUpgrade(final int oldVersion, final int newVersion) { 194 if (oldVersion > newVersion) { 195 throw new IllegalStateException( 196 "You appear to have downgraded your app. Please clear app data."); 197 } else if (oldVersion == newVersion) { 198 return; 199 } 200 } 201 202 @Override 203 protected boolean canBackup(final String key) { 204 return PreferenceKeys.BACKUP_KEYS.contains(key); 205 } 206 207 @Override 208 protected boolean hasMigrationCompleted() { 209 return getSharedPreferences().getInt(PreferenceKeys.MIGRATED_VERSION, 0) 210 >= CURRENT_VERSION_NUMBER; 211 } 212 213 @Override 214 protected void setMigrationComplete() { 215 getEditor().putInt(PreferenceKeys.MIGRATED_VERSION, CURRENT_VERSION_NUMBER).commit(); 216 } 217 218 public boolean isWidgetConfigured(int appWidgetId) { 219 return getSharedPreferences().contains(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + appWidgetId); 220 } 221 222 public void configureWidget(int appWidgetId, Account account, final String folderUri) { 223 if (account == null) { 224 LogUtils.e(LOG_TAG, "Cannot configure widget with null account"); 225 return; 226 } 227 getEditor().putString(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + appWidgetId, 228 createWidgetPreferenceValue(account, folderUri)).apply(); 229 } 230 231 public String getWidgetConfiguration(int appWidgetId) { 232 return getSharedPreferences().getString(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + appWidgetId, 233 null); 234 } 235 236 private static String createWidgetPreferenceValue(Account account, String folderUri) { 237 return account.uri.toString() + BaseWidgetProvider.ACCOUNT_FOLDER_PREFERENCE_SEPARATOR 238 + folderUri; 239 240 } 241 242 public void clearWidgets(int[] appWidgetIds) { 243 for (int id : appWidgetIds) { 244 getEditor().remove(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + id); 245 } 246 getEditor().apply(); 247 } 248 249 /** If <code>true</code>, we should default all replies to "reply all" rather than "reply" */ 250 public boolean getDefaultReplyAll() { 251 return getSharedPreferences().getBoolean(PreferenceKeys.DEFAULT_REPLY_ALL, false); 252 } 253 254 public void setDefaultReplyAll(final boolean replyAll) { 255 getEditor().putBoolean(PreferenceKeys.DEFAULT_REPLY_ALL, replyAll).apply(); 256 notifyBackupPreferenceChanged(); 257 } 258 259 /** 260 * Returns a string indicating the preferred removal action. 261 * Should be one of the {@link RemovalActions}. 262 */ 263 public String getRemovalAction(final boolean supportsArchive) { 264 if (!supportsArchive) { 265 return RemovalActions.DELETE; 266 } 267 268 final SharedPreferences sharedPreferences = getSharedPreferences(); 269 final String removalAction = 270 sharedPreferences.getString(PreferenceKeys.REMOVAL_ACTION, null); 271 if (TextUtils.equals(removalAction, RemovalActions.ARCHIVE_AND_DELETE)) { 272 return RemovalActions.ARCHIVE; 273 } 274 return sharedPreferences.getString(PreferenceKeys.REMOVAL_ACTION, 275 RemovalActions.ARCHIVE); 276 } 277 278 /** 279 * Sets the removal action preference. 280 * @param removalAction The preferred {@link RemovalActions}. 281 */ 282 public void setRemovalAction(final @RemovalActionTypes String removalAction) { 283 getEditor().putString(PreferenceKeys.REMOVAL_ACTION, removalAction).apply(); 284 notifyBackupPreferenceChanged(); 285 } 286 287 /** 288 * Gets a boolean indicating whether conversation list swiping is enabled. 289 */ 290 public boolean getIsConversationListSwipeEnabled() { 291 final SharedPreferences sharedPreferences = getSharedPreferences(); 292 return sharedPreferences.getBoolean(PreferenceKeys.CONVERSATION_LIST_SWIPE, true); 293 } 294 295 public void setConversationListSwipeEnabled(final boolean enabled) { 296 getEditor().putBoolean(PreferenceKeys.CONVERSATION_LIST_SWIPE, enabled).apply(); 297 notifyBackupPreferenceChanged(); 298 } 299 300 /** 301 * Gets the action to take (one of the values from {@link UIProvider.Swipe}) when an item in the 302 * conversation list is swiped. 303 * 304 * @param allowArchive <code>true</code> if Archive is an acceptable action (this will affect 305 * the default return value) 306 */ 307 public int getConversationListSwipeActionInteger(final boolean allowArchive) { 308 final boolean swipeEnabled = getIsConversationListSwipeEnabled(); 309 final boolean archive = !RemovalActions.DELETE.equals(getRemovalAction(allowArchive)); 310 311 if (swipeEnabled) { 312 return archive ? UIProvider.Swipe.ARCHIVE : UIProvider.Swipe.DELETE; 313 } 314 315 return UIProvider.Swipe.DISABLED; 316 } 317 318 /** 319 * Returns the previously cached notification set 320 */ 321 public Set<String> getActiveNotificationSet() { 322 return getSharedPreferences() 323 .getStringSet(PreferenceKeys.CACHED_ACTIVE_NOTIFICATION_SET, null); 324 } 325 326 /** 327 * Caches the current notification set. 328 */ 329 public void cacheActiveNotificationSet(final Set<String> notificationSet) { 330 getEditor().putStringSet(PreferenceKeys.CACHED_ACTIVE_NOTIFICATION_SET, notificationSet) 331 .apply(); 332 } 333 334 /** 335 * Returns whether the teaser has been shown before 336 */ 337 public boolean isConversationPhotoTeaserAlreadyShown() { 338 return getSharedPreferences() 339 .getBoolean(PreferenceKeys.CONVERSATION_PHOTO_TEASER_SHOWN, false); 340 } 341 342 /** 343 * Notify that we have shown the teaser 344 */ 345 public void setConversationPhotoTeaserAlreadyShown() { 346 getEditor().putBoolean(PreferenceKeys.CONVERSATION_PHOTO_TEASER_SHOWN, true).apply(); 347 } 348 349 /** 350 * Returns whether the tip has been shown before 351 */ 352 public boolean isLongPressToSelectTipAlreadyShown() { 353 // Using an int instead of boolean here in case we need to reshow the tip (don't have 354 // to use a new preference name). 355 return getSharedPreferences() 356 .getInt(PreferenceKeys.LONG_PRESS_TO_SELECT_TIP_SHOWN, 0) > 0; 357 } 358 359 public void setLongPressToSelectTipAlreadyShown() { 360 getEditor().putInt(PreferenceKeys.LONG_PRESS_TO_SELECT_TIP_SHOWN, 1).apply(); 361 notifyBackupPreferenceChanged(); 362 } 363 364 public void setSenderWhitelist(Set<String> addresses) { 365 getEditor().putStringSet(PreferenceKeys.DISPLAY_IMAGES, addresses).apply(); 366 notifyBackupPreferenceChanged(); 367 } 368 public void setSenderWhitelistPatterns(Set<String> patterns) { 369 getEditor().putStringSet(PreferenceKeys.DISPLAY_IMAGES_PATTERNS, patterns).apply(); 370 notifyBackupPreferenceChanged(); 371 } 372 373 /** 374 * Returns whether or not an email address is in the whitelist of senders to show images for. 375 * This method reads the entire whitelist, so if you have multiple emails to check, you should 376 * probably call getSenderWhitelist() and check membership yourself. 377 * 378 * @param sender raw email address ("foo (at) bar.com") 379 * @return whether we should show pictures for this sender 380 */ 381 public boolean getDisplayImagesFromSender(String sender) { 382 boolean displayImages = getSenderWhitelist().contains(sender); 383 if (!displayImages) { 384 final SharedPreferences sharedPreferences = getSharedPreferences(); 385 // Check the saved email address patterns to determine if this pattern matches 386 final Set<String> defaultPatternSet = Collections.emptySet(); 387 final Set<String> currentPatterns = sharedPreferences.getStringSet( 388 PreferenceKeys.DISPLAY_IMAGES_PATTERNS, defaultPatternSet); 389 for (String pattern : currentPatterns) { 390 displayImages = Pattern.compile(pattern).matcher(sender).matches(); 391 if (displayImages) { 392 break; 393 } 394 } 395 } 396 397 return displayImages; 398 } 399 400 401 public void setDisplayImagesFromSender(String sender, List<Pattern> allowedPatterns) { 402 if (allowedPatterns != null) { 403 // Look at the list of patterns where we want to allow a particular class of 404 // email address 405 for (Pattern pattern : allowedPatterns) { 406 if (pattern.matcher(sender).matches()) { 407 // The specified email address matches one of the social network patterns. 408 // Save the pattern itself 409 final Set<String> currentPatterns = getSenderWhitelistPatterns(); 410 final String patternRegex = pattern.pattern(); 411 if (!currentPatterns.contains(patternRegex)) { 412 // Copy strings to a modifiable set 413 final Set<String> updatedPatterns = Sets.newHashSet(currentPatterns); 414 updatedPatterns.add(patternRegex); 415 setSenderWhitelistPatterns(updatedPatterns); 416 } 417 return; 418 } 419 } 420 } 421 final Set<String> whitelist = getSenderWhitelist(); 422 if (!whitelist.contains(sender)) { 423 // Storing a JSONObject is slightly more nice in that maps are guaranteed to not have 424 // duplicate entries, but using a Set as intermediate representation guarantees this 425 // for us anyway. Also, using maps to represent sets forces you to pick values for 426 // them, and that's weird. 427 final Set<String> updatedList = Sets.newHashSet(whitelist); 428 updatedList.add(sender); 429 setSenderWhitelist(updatedList); 430 } 431 } 432 433 private Set<String> getSenderWhitelist() { 434 final SharedPreferences sharedPreferences = getSharedPreferences(); 435 final Set<String> defaultAddressSet = Collections.emptySet(); 436 return sharedPreferences.getStringSet(PreferenceKeys.DISPLAY_IMAGES, defaultAddressSet); 437 } 438 439 440 private Set<String> getSenderWhitelistPatterns() { 441 final SharedPreferences sharedPreferences = getSharedPreferences(); 442 final Set<String> defaultPatternSet = Collections.emptySet(); 443 return sharedPreferences.getStringSet(PreferenceKeys.DISPLAY_IMAGES_PATTERNS, 444 defaultPatternSet); 445 } 446 447 public void clearSenderWhiteList() { 448 final SharedPreferences.Editor editor = getEditor(); 449 editor.putStringSet(PreferenceKeys.DISPLAY_IMAGES, Collections.EMPTY_SET); 450 editor.putStringSet(PreferenceKeys.DISPLAY_IMAGES_PATTERNS, Collections.EMPTY_SET); 451 editor.apply(); 452 } 453 454 public void setShowSenderImages(boolean enable) { 455 getEditor().putBoolean(PreferenceKeys.SHOW_SENDER_IMAGES, enable).apply(); 456 notifyBackupPreferenceChanged(); 457 } 458 459 public boolean getShowSenderImages() { 460 final SharedPreferences sharedPreferences = getSharedPreferences(); 461 return sharedPreferences.getBoolean(PreferenceKeys.SHOW_SENDER_IMAGES, true); 462 } 463 464 public int getNumOfDismissesForAutoSyncOff() { 465 return getSharedPreferences().getInt(PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, 0); 466 } 467 468 public void resetNumOfDismissesForAutoSyncOff() { 469 final int value = getSharedPreferences().getInt( 470 PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, 0); 471 if (value != 0) { 472 getEditor().putInt(PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, 0).apply(); 473 } 474 } 475 476 public void incNumOfDismissesForAutoSyncOff() { 477 final int value = getSharedPreferences().getInt( 478 PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, 0); 479 getEditor().putInt(PreferenceKeys.GLOBAL_SYNC_OFF_DISMISSES, value + 1).apply(); 480 } 481 482 public void setConfirmDelete(final boolean confirmDelete) { 483 getEditor().putBoolean(PreferenceKeys.CONFIRM_DELETE, confirmDelete).apply(); 484 notifyBackupPreferenceChanged(); 485 } 486 487 public boolean getConfirmDelete() { 488 return getSharedPreferences().getBoolean(PreferenceKeys.CONFIRM_DELETE, false); 489 } 490 491 public void setConfirmArchive(final boolean confirmArchive) { 492 getEditor().putBoolean(PreferenceKeys.CONFIRM_ARCHIVE, confirmArchive).apply(); 493 notifyBackupPreferenceChanged(); 494 } 495 496 public boolean getConfirmArchive() { 497 return getSharedPreferences().getBoolean(PreferenceKeys.CONFIRM_ARCHIVE, false); 498 } 499 500 public void setConfirmSend(final boolean confirmSend) { 501 getEditor().putBoolean(PreferenceKeys.CONFIRM_SEND, confirmSend).apply(); 502 notifyBackupPreferenceChanged(); 503 } 504 505 public boolean getConfirmSend() { 506 return getSharedPreferences().getBoolean(PreferenceKeys.CONFIRM_SEND, false); 507 } 508 509 public void setAutoAdvanceMode(final int mode) { 510 getEditor().putInt(PreferenceKeys.AUTO_ADVANCE_MODE, mode).apply(); 511 notifyBackupPreferenceChanged(); 512 } 513 514 public int getAutoAdvanceMode() { 515 return getSharedPreferences() 516 .getInt(PreferenceKeys.AUTO_ADVANCE_MODE, UIProvider.AutoAdvance.DEFAULT); 517 } 518 519 public void setConversationOverviewMode(final boolean overviewMode) { 520 getEditor().putBoolean(PreferenceKeys.CONVERSATION_OVERVIEW_MODE, overviewMode).apply(); 521 } 522 523 public boolean getConversationOverviewMode() { 524 return getSharedPreferences() 525 .getBoolean(PreferenceKeys.CONVERSATION_OVERVIEW_MODE, true); 526 } 527 528 public boolean isConversationOverviewModeSet() { 529 return getSharedPreferences().contains(PreferenceKeys.CONVERSATION_OVERVIEW_MODE); 530 } 531 532 public void setAlwaysLaunchGmailFromEmailTombstone(final boolean alwaysLaunchGmail) { 533 getEditor() 534 .putBoolean(PreferenceKeys.ALWAYS_LAUNCH_GMAIL_FROM_EMAIL_TOMBSTONE, 535 alwaysLaunchGmail) 536 .apply(); 537 } 538 539 public boolean getAlwaysLaunchGmailFromEmailTombstone() { 540 return getSharedPreferences() 541 .getBoolean(PreferenceKeys.ALWAYS_LAUNCH_GMAIL_FROM_EMAIL_TOMBSTONE, false); 542 } 543 544 public void setSnapHeaderMode(final int snapHeaderMode) { 545 getEditor().putInt(PreferenceKeys.SNAP_HEADER_MODE, snapHeaderMode).apply(); 546 } 547 548 public int getSnapHeaderMode() { 549 return getSharedPreferences() 550 .getInt(PreferenceKeys.SNAP_HEADER_MODE, mSnapHeaderDefault); 551 } 552 553 public int getSnapHeaderDefault() { 554 return mSnapHeaderDefault; 555 } 556 557 public int getMigrationState() { 558 return getSharedPreferences() 559 .getInt(PreferenceKeys.MIGRATION_STATE, PreferenceKeys.MIGRATION_STATE_NONE); 560 } 561 562 public void setMigrationState(final int state) { 563 getEditor().putInt(PreferenceKeys.MIGRATION_STATE, state).apply(); 564 } 565 566 public Set<String> getRecentAccounts() { 567 return getSharedPreferences().getStringSet(PreferenceKeys.RECENT_ACCOUNTS, null); 568 } 569 570 public void setRecentAccounts(Set<String> recentAccounts) { 571 getEditor().putStringSet(PreferenceKeys.RECENT_ACCOUNTS, recentAccounts).apply(); 572 } 573 574 /** 575 * Returns the minimum version number of the {@link com.android.mail.utils.HtmlSanitizer} which 576 * is trusted. If the version of the HtmlSanitizer does not meet or exceed this value, 577 * sanitization will be deemed untrustworthy and emails will be displayed in a sandbox that does 578 * not allow script execution. 579 */ 580 public int getRequiredSanitizerVersionNumber() { 581 return getSharedPreferences().getInt(PreferenceKeys.REQUIRED_SANITIZER_VERSION_NUMBER, 1); 582 } 583 584 /** 585 * @param versionNumber the minimum version number of the 586 * {@link com.android.mail.utils.HtmlSanitizer} which produces trusted output 587 */ 588 public void setRequiredSanitizerVersionNumber(int versionNumber) { 589 getEditor().putInt(PreferenceKeys.REQUIRED_SANITIZER_VERSION_NUMBER, versionNumber).apply(); 590 } 591 592 /** 593 * Returns the latest time the number of accounts in the application was sent to Analyitcs. 594 * @return the datetime in epoch milliseconds. 595 */ 596 public long getNbAccountsLatestReport() { 597 return getSharedPreferences().getLong(PreferenceKeys.ANALYTICS_NB_ACCOUNT_LATEST_REPORT, 0); 598 } 599 600 /** 601 * Set the latest time the number of accounts in the application was sent to Analytics. 602 */ 603 public void setNbAccountsLatestReport(long timeMs) { 604 getEditor().putLong( 605 PreferenceKeys.ANALYTICS_NB_ACCOUNT_LATEST_REPORT, timeMs); 606 } 607 } 608