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
18package com.android.mail.preferences;
19
20import android.content.Context;
21import android.content.SharedPreferences;
22import android.support.annotation.StringDef;
23import android.text.TextUtils;
24
25import com.android.mail.R;
26import com.android.mail.providers.Account;
27import com.android.mail.providers.UIProvider;
28import com.android.mail.utils.LogUtils;
29import com.android.mail.widget.BaseWidgetProvider;
30import com.google.common.annotations.VisibleForTesting;
31import com.google.common.collect.ImmutableSet;
32import com.google.common.collect.Sets;
33
34import java.lang.annotation.Retention;
35import java.lang.annotation.RetentionPolicy;
36import java.util.Collections;
37import java.util.List;
38import java.util.Set;
39import 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 */
46public 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@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