MailAppProvider.java revision 828f46144e79c7bb0856f43dc237e4faef3623de
1/**
2 * Copyright (c) 2011, Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.mail.providers;
18
19import android.app.Activity;
20import android.content.ContentProvider;
21import android.content.ContentResolver;
22import android.content.ContentValues;
23import android.content.Context;
24import android.content.CursorLoader;
25import android.content.Intent;
26import android.content.Loader;
27import android.content.Loader.OnLoadCompleteListener;
28import android.content.SharedPreferences;
29import android.database.Cursor;
30import android.database.MatrixCursor;
31import android.net.Uri;
32import android.os.Bundle;
33import android.provider.BaseColumns;
34import android.text.TextUtils;
35
36import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
37import com.android.mail.providers.protos.boot.AccountReceiver;
38import com.android.mail.utils.LogTag;
39import com.android.mail.utils.LogUtils;
40import com.android.mail.utils.MatrixCursorWithExtra;
41import com.google.common.collect.ImmutableSet;
42import com.google.common.collect.Maps;
43import com.google.common.collect.Sets;
44
45import java.util.Collections;
46import java.util.Map;
47import java.util.Set;
48import java.util.regex.Pattern;
49
50
51
52/**
53 * The Account Cache provider allows email providers to register "accounts" and the UI has a single
54 * place to query for the list of accounts.
55 *
56 * During development this will allow new account types to be added, and allow them to be shown in
57 * the application.  For example, the mock accounts can be enabled/disabled.
58 * In the future, once other processes can add new accounts, this could allow other "mail"
59 * applications have their content appear within the application
60 */
61public abstract class MailAppProvider extends ContentProvider
62        implements OnLoadCompleteListener<Cursor>{
63
64    private static final String SHARED_PREFERENCES_NAME = "MailAppProvider";
65    private static final String ACCOUNT_LIST_KEY = "accountList";
66    private static final String LAST_VIEWED_ACCOUNT_KEY = "lastViewedAccount";
67
68    /**
69     * Extra used in the result from the activity launched by the intent specified
70     * by {@link #getNoAccountsIntent} to return the list of accounts.  The data
71     * specified by this extra key should be a ParcelableArray.
72     */
73    public static final String ADD_ACCOUNT_RESULT_ACCOUNTS_EXTRA = "addAccountResultAccounts";
74
75    private final static String LOG_TAG = LogTag.getLogTag();
76
77    private final Map<Uri, AccountCacheEntry> mAccountCache = Maps.newHashMap();
78
79    private final Map<Uri, CursorLoader> mCursorLoaderMap = Maps.newHashMap();
80
81    private ContentResolver mResolver;
82    private static String sAuthority;
83    private static MailAppProvider sInstance;
84
85    private volatile boolean mAccountsFullyLoaded = false;
86
87    private SharedPreferences mSharedPrefs;
88
89    /**
90     * Allows the implementing provider to specify the authority for this provider. Email and Gmail
91     * must specify different authorities.
92     */
93    protected abstract String getAuthority();
94
95    /**
96     * Authority for the suggestions provider. Email and Gmail must specify different authorities,
97     * much like the implementation of {@link #getAuthority()}.
98     * @return the suggestion authority associated with this provider.
99     */
100    public abstract String getSuggestionAuthority();
101
102    /**
103     * Allows the implementing provider to specify an intent that should be used in a call to
104     * {@link Context#startActivityForResult(android.content.Intent)} when the account provider
105     * doesn't return any accounts.
106     *
107     * The result from the {@link Activity} activity should include the list of accounts in
108     * the returned intent, in the
109
110     * @return Intent or null, if the provider doesn't specify a behavior when no accounts are
111     * specified.
112     */
113    protected abstract Intent getNoAccountsIntent(Context context);
114
115    /**
116     * The cursor returned from a call to {@link android.content.ContentResolver#query()} with this
117     * uri will return a cursor that with columns that are a subset of the columns specified
118     * in {@link UIProvider.ConversationColumns}
119     * The cursor returned by this query can return a {@link android.os.Bundle}
120     * from a call to {@link android.database.Cursor#getExtras()}.  This Bundle may have
121     * values with keys listed in {@link AccountCursorExtraKeys}
122     */
123    public static Uri getAccountsUri() {
124        return Uri.parse("content://" + sAuthority + "/");
125    }
126
127    public static MailAppProvider getInstance() {
128        return sInstance;
129    }
130
131    /** Default constructor */
132    public MailAppProvider() {
133        sInstance = this;
134    }
135
136    @Override
137    public boolean onCreate() {
138        sAuthority = getAuthority();
139        mResolver = getContext().getContentResolver();
140
141        final Intent intent = new Intent(AccountReceiver.ACTION_PROVIDER_CREATED);
142        getContext().sendBroadcast(intent);
143
144        // Load the previously saved account list
145        loadCachedAccountList();
146
147        return true;
148    }
149
150    @Override
151    public void shutdown() {
152        sInstance = null;
153
154        for (CursorLoader loader : mCursorLoaderMap.values()) {
155            loader.stopLoading();
156        }
157        mCursorLoaderMap.clear();
158    }
159
160    @Override
161    public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs,
162            String sortOrder) {
163        // This content provider currently only supports one query (to return the list of accounts).
164        // No reason to check the uri.  Currently only checking the projections
165
166        // Validates and returns the projection that should be used.
167        final String[] resultProjection = UIProviderValidator.validateAccountProjection(projection);
168        final Bundle extras = new Bundle();
169        extras.putInt(AccountCursorExtraKeys.ACCOUNTS_LOADED, mAccountsFullyLoaded ? 1 : 0);
170
171        // Make a copy of the account cache
172
173        final Set<AccountCacheEntry> accountList;
174        synchronized (mAccountCache) {
175            accountList = ImmutableSet.copyOf(mAccountCache.values());
176        }
177
178        final MatrixCursor cursor =
179                new MatrixCursorWithExtra(resultProjection, accountList.size(), extras);
180
181        for (AccountCacheEntry accountEntry : accountList) {
182            final Account account = accountEntry.mAccount;
183            final MatrixCursor.RowBuilder builder = cursor.newRow();
184
185            for (String column : resultProjection) {
186                if (TextUtils.equals(column, BaseColumns._ID)) {
187                    // TODO(pwestbro): remove this as it isn't used.
188                    builder.add(Integer.valueOf(0));
189                } else if (TextUtils.equals(column, UIProvider.AccountColumns.NAME)) {
190                    builder.add(account.name);
191                } else if (TextUtils.equals(column, UIProvider.AccountColumns.PROVIDER_VERSION)) {
192                    // TODO fix this
193                    builder.add(Integer.valueOf(account.providerVersion));
194                } else if (TextUtils.equals(column, UIProvider.AccountColumns.URI)) {
195                    builder.add(account.uri);
196                } else if (TextUtils.equals(column, UIProvider.AccountColumns.CAPABILITIES)) {
197                    builder.add(Integer.valueOf(account.capabilities));
198                } else if (TextUtils.equals(column, UIProvider.AccountColumns.FOLDER_LIST_URI)) {
199                    builder.add(account.folderListUri);
200                } else if (TextUtils
201                        .equals(column, UIProvider.AccountColumns.FULL_FOLDER_LIST_URI)) {
202                    builder.add(account.fullFolderListUri);
203                } else if (TextUtils.equals(column, UIProvider.AccountColumns.SEARCH_URI)) {
204                    builder.add(account.searchUri);
205                } else if (TextUtils.equals(column,
206                        UIProvider.AccountColumns.ACCOUNT_FROM_ADDRESSES)) {
207                    builder.add(account.accountFromAddresses);
208                } else if (TextUtils.equals(column, UIProvider.AccountColumns.SAVE_DRAFT_URI)) {
209                    builder.add(account.saveDraftUri);
210                } else if (TextUtils.equals(column, UIProvider.AccountColumns.SEND_MAIL_URI)) {
211                    builder.add(account.sendMessageUri);
212                } else if (TextUtils.equals(column,
213                        UIProvider.AccountColumns.EXPUNGE_MESSAGE_URI)) {
214                    builder.add(account.expungeMessageUri);
215                } else if (TextUtils.equals(column, UIProvider.AccountColumns.UNDO_URI)) {
216                    builder.add(account.undoUri);
217                } else if (TextUtils.equals(column,
218                        UIProvider.AccountColumns.SETTINGS_INTENT_URI)) {
219                    builder.add(account.settingsIntentUri);
220                } else if (TextUtils.equals(column,
221                        UIProvider.AccountColumns.HELP_INTENT_URI)) {
222                    builder.add(account.helpIntentUri);
223                } else if (TextUtils.equals(column,
224                        UIProvider.AccountColumns.SEND_FEEDBACK_INTENT_URI)) {
225                    builder.add(account.sendFeedbackIntentUri);
226                } else if (TextUtils.equals(column, UIProvider.AccountColumns.SYNC_STATUS)) {
227                    builder.add(Integer.valueOf(account.syncStatus));
228                } else if (TextUtils.equals(column, UIProvider.AccountColumns.COMPOSE_URI)) {
229                    builder.add(account.composeIntentUri);
230                } else if (TextUtils.equals(column, UIProvider.AccountColumns.MIME_TYPE)) {
231                    builder.add(account.mimeType);
232                } else if (TextUtils.equals(column,
233                        UIProvider.AccountColumns.RECENT_FOLDER_LIST_URI)) {
234                    builder.add(account.recentFolderListUri);
235                } else if (TextUtils.equals(column,
236                        UIProvider.AccountColumns.DEFAULT_RECENT_FOLDER_LIST_URI)) {
237                    builder.add(account.defaultRecentFolderListUri);
238                } else if (TextUtils.equals(column,
239                        UIProvider.AccountColumns.MANUAL_SYNC_URI)) {
240                    builder.add(account.manualSyncUri);
241                } else if (TextUtils.equals(column, UIProvider.AccountColumns.COLOR)) {
242                    builder.add(account.color);
243                } else if (TextUtils.equals(column,
244                        UIProvider.AccountColumns.SettingsColumns.SIGNATURE)) {
245                    builder.add(account.settings.signature);
246                } else if (TextUtils.equals(column,
247                        UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)) {
248                    builder.add(Integer.valueOf(account.settings.autoAdvance));
249                } else if (TextUtils.equals(column,
250                        UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE)) {
251                    builder.add(Integer.valueOf(account.settings.messageTextSize));
252                } else if (TextUtils.equals(column,
253                        UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)) {
254                    builder.add(Integer.valueOf(account.settings.replyBehavior));
255                } else if (TextUtils.equals(column,
256                        UIProvider.AccountColumns.SettingsColumns.HIDE_CHECKBOXES)) {
257                    builder.add(Integer.valueOf(account.settings.hideCheckboxes ? 1 : 0));
258                } else if (TextUtils.equals(column,
259                        UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) {
260                    builder.add(Integer.valueOf(account.settings.confirmDelete ? 1 : 0));
261                } else if (TextUtils.equals(column,
262                        UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)) {
263                    builder.add(Integer.valueOf(account.settings.confirmArchive ? 1 : 0));
264                } else if (TextUtils.equals(column,
265                        UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) {
266                    builder.add(Integer.valueOf(account.settings.confirmSend ? 1 : 0));
267                } else if (TextUtils.equals(column,
268                        UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)) {
269                    builder.add(account.settings.defaultInbox);
270                } else if (TextUtils.equals(column,
271                        UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME)) {
272                    builder.add(account.settings.defaultInboxName);
273                } else if (TextUtils.equals(column,
274                        UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS)) {
275                    builder.add(Integer.valueOf(account.settings.snapHeaders));
276                } else if (TextUtils.equals(column,
277                        UIProvider.AccountColumns.SettingsColumns.FORCE_REPLY_FROM_DEFAULT)) {
278                    builder.add(Integer.valueOf(account.settings.forceReplyFromDefault ? 1 : 0));
279                } else if (TextUtils.equals(column,
280                        UIProvider.AccountColumns.SettingsColumns.MAX_ATTACHMENT_SIZE)) {
281                    builder.add(account.settings.maxAttachmentSize);
282                } else {
283                    throw new IllegalStateException("Column not found: " + column);
284                }
285            }
286        }
287
288        cursor.setNotificationUri(mResolver, getAccountsUri());
289        return cursor;
290    }
291
292    @Override
293    public Uri insert(Uri url, ContentValues values) {
294        return url;
295    }
296
297    @Override
298    public int update(Uri url, ContentValues values, String selection,
299            String[] selectionArgs) {
300        return 0;
301    }
302
303    @Override
304    public int delete(Uri url, String selection, String[] selectionArgs) {
305        return 0;
306    }
307
308    @Override
309    public String getType(Uri uri) {
310        return null;
311    }
312
313    /**
314     * Asynchronously ads all of the accounts that are specified by the result set returned by
315     * {@link ContentProvider#query()} for the specified uri.  The content provider handling the
316     * query needs to handle the {@link UIProvider.ACCOUNTS_PROJECTION}
317     * Any changes to the underlying provider will automatically be reflected.
318     * @param resolver
319     * @param accountsQueryUri
320     */
321    public static void addAccountsForUriAsync(Uri accountsQueryUri) {
322        getInstance().startAccountsLoader(accountsQueryUri);
323    }
324
325    /**
326     * Returns the intent that should be used in a call to
327     * {@link Context#startActivity(android.content.Intent)} when the account provider doesn't
328     * return any accounts
329     * @return Intent or null, if the provider doesn't specify a behavior when no acccounts are
330     * specified.
331     */
332    public static Intent getNoAccountIntent(Context context) {
333        return getInstance().getNoAccountsIntent(context);
334    }
335
336    private synchronized void startAccountsLoader(Uri accountsQueryUri) {
337        final CursorLoader accountsCursorLoader = new CursorLoader(getContext(), accountsQueryUri,
338                UIProvider.ACCOUNTS_PROJECTION, null, null, null);
339
340        // Listen for the results
341        accountsCursorLoader.registerListener(accountsQueryUri.hashCode(), this);
342        accountsCursorLoader.startLoading();
343
344        // If there is a previous loader for the given uri, stop it
345        final CursorLoader oldLoader = mCursorLoaderMap.get(accountsQueryUri);
346        if (oldLoader != null) {
347            oldLoader.stopLoading();
348        }
349        mCursorLoaderMap.put(accountsQueryUri, accountsCursorLoader);
350    }
351
352    public static void addAccount(Account account, Uri accountsQueryUri) {
353        final MailAppProvider provider = getInstance();
354        if (provider == null) {
355            throw new IllegalStateException("MailAppProvider not intialized");
356        }
357        provider.addAccountImpl(account, accountsQueryUri, true /* notify */);
358    }
359
360    private void addAccountImpl(Account account, Uri accountsQueryUri, boolean notify) {
361        synchronized (mAccountCache) {
362            if (account != null) {
363                LogUtils.v(LOG_TAG, "adding account %s", account);
364                mAccountCache.put(account.uri, new AccountCacheEntry(account, accountsQueryUri));
365            }
366        }
367        // Explicitly calling this out of the synchronized block in case any of the observers get
368        // called synchronously.
369        if (notify) {
370            broadcastAccountChange();
371        }
372
373        // Cache the updated account list
374        cacheAccountList();
375    }
376
377    public static void removeAccount(Uri accountUri) {
378        final MailAppProvider provider = getInstance();
379        if (provider == null) {
380            throw new IllegalStateException("MailAppProvider not intialized");
381        }
382        provider.removeAccounts(Collections.singleton(accountUri), true /* notify */);
383    }
384
385    private void removeAccounts(Set<Uri> uris, boolean notify) {
386        synchronized (mAccountCache) {
387            for (Uri accountUri : uris) {
388                mAccountCache.remove(accountUri);
389            }
390        }
391
392        // Explicitly calling this out of the synchronized block in case any of the observers get
393        // called synchronously.
394        if (notify) {
395            broadcastAccountChange();
396        }
397
398        // Cache the updated account list
399        cacheAccountList();
400    }
401
402    private static void broadcastAccountChange() {
403        final MailAppProvider provider = sInstance;
404
405        if (provider != null) {
406            provider.mResolver.notifyChange(getAccountsUri(), null);
407        }
408    }
409
410    /**
411     * Returns the {@link Account#uri} (in String form) of the last viewed account.
412     */
413    public String getLastViewedAccount() {
414        return getPreferences().getString(LAST_VIEWED_ACCOUNT_KEY, null);
415    }
416
417    /**
418     * Persists the {@link Account#uri} (in String form) of the last viewed account.
419     */
420    public void setLastViewedAccount(String accountUriStr) {
421        final SharedPreferences.Editor editor = getPreferences().edit();
422        editor.putString(LAST_VIEWED_ACCOUNT_KEY, accountUriStr);
423        editor.apply();
424    }
425
426    private void loadCachedAccountList() {
427        final SharedPreferences preference = getPreferences();
428
429        final Set<String> accountsStringSet = preference.getStringSet(ACCOUNT_LIST_KEY, null);
430
431        if (accountsStringSet != null) {
432            for (String serializedAccount : accountsStringSet) {
433                try {
434                    final AccountCacheEntry accountEntry =
435                            new AccountCacheEntry(serializedAccount);
436                    if (accountEntry.mAccount.settings != null) {
437                        addAccountImpl(accountEntry.mAccount, accountEntry.mAccountsQueryUri,
438                                false /* don't notify */);
439                    } else {
440                        LogUtils.e(LOG_TAG, "Dropping account that doesn't specify settings");
441                    }
442                } catch (Exception e) {
443                    // Unable to create account object, skip to next
444                    LogUtils.e(LOG_TAG, e,
445                            "Unable to create account object from serialized string '%s'",
446                            serializedAccount);
447                }
448            }
449            broadcastAccountChange();
450        }
451    }
452
453    private void cacheAccountList() {
454        final SharedPreferences preference = getPreferences();
455
456        final Set<AccountCacheEntry> accountList;
457        synchronized (mAccountCache) {
458            accountList = ImmutableSet.copyOf(mAccountCache.values());
459        }
460
461        final Set<String> serializedAccounts = Sets.newHashSet();
462        for (AccountCacheEntry accountEntry : accountList) {
463            serializedAccounts.add(accountEntry.serialize());
464        }
465
466        final SharedPreferences.Editor editor = getPreferences().edit();
467        editor.putStringSet(ACCOUNT_LIST_KEY, serializedAccounts);
468        editor.apply();
469    }
470
471    private SharedPreferences getPreferences() {
472        if (mSharedPrefs == null) {
473            mSharedPrefs = getContext().getSharedPreferences(
474                    SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
475        }
476        return mSharedPrefs;
477    }
478
479    static public Account getAccountFromAccountUri(Uri accountUri) {
480        MailAppProvider provider = getInstance();
481        if (provider != null && provider.mAccountsFullyLoaded) {
482            synchronized(provider.mAccountCache) {
483                AccountCacheEntry entry = provider.mAccountCache.get(accountUri);
484                if (entry != null) {
485                    return entry.mAccount;
486                }
487            }
488        }
489        return null;
490    }
491
492    @Override
493    public void onLoadComplete(Loader<Cursor> loader, Cursor data) {
494        if (data == null) {
495            LogUtils.d(LOG_TAG, "null account cursor returned");
496            return;
497        }
498
499        LogUtils.d(LOG_TAG, "Cursor with %d accounts returned", data.getCount());
500        final CursorLoader cursorLoader = (CursorLoader)loader;
501        final Uri accountsQueryUri = cursorLoader.getUri();
502
503        final Set<AccountCacheEntry> accountList;
504        synchronized (mAccountCache) {
505            accountList = ImmutableSet.copyOf(mAccountCache.values());
506        }
507
508        // Build a set of the account uris that had been associated with that query
509        final Set<Uri> previousQueryUriMap = Sets.newHashSet();
510        for (AccountCacheEntry entry : accountList) {
511            if (accountsQueryUri.equals(entry.mAccountsQueryUri)) {
512                previousQueryUriMap.add(entry.mAccount.uri);
513            }
514        }
515
516        // Update the internal state of this provider if the returned result set
517        // represents all accounts
518        // TODO: determine what should happen with a heterogeneous set of accounts
519        final Bundle extra = data.getExtras();
520        mAccountsFullyLoaded = extra.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
521
522        final Set<Uri> newQueryUriMap = Sets.newHashSet();
523        while (data.moveToNext()) {
524            final Account account = new Account(data);
525            final Uri accountUri = account.uri;
526            newQueryUriMap.add(accountUri);
527            addAccountImpl(account, accountsQueryUri, false /* don't notify */);
528        }
529
530        if (previousQueryUriMap != null) {
531            // Remove all of the accounts that are in the new result set
532            previousQueryUriMap.removeAll(newQueryUriMap);
533
534            // For all of the entries that had been in the previous result set, and are not
535            // in the new result set, remove them from the cache
536            if (previousQueryUriMap.size() > 0 && mAccountsFullyLoaded) {
537              removeAccounts(previousQueryUriMap, false /* don't notify */);
538            }
539        }
540        broadcastAccountChange();
541    }
542
543    /**
544     * Object that allows the Account Cache provider to associate the account with the content
545     * provider uri that originated that account.
546     */
547    private static class AccountCacheEntry {
548        final Account mAccount;
549        final Uri mAccountsQueryUri;
550
551        private static final String ACCOUNT_ENTRY_COMPONENT_SEPARATOR = "^**^";
552        private static final Pattern ACCOUNT_ENTRY_COMPONENT_SEPARATOR_PATTERN =
553                Pattern.compile("\\^\\*\\*\\^");
554
555        private static final int NUMBER_MEMBERS = 2;
556
557        public AccountCacheEntry(Account account, Uri accountQueryUri) {
558            mAccount = account;
559            mAccountsQueryUri = accountQueryUri;
560        }
561
562        /**
563         * Return a serialized String for this AccountCacheEntry.
564         */
565        public synchronized String serialize() {
566            StringBuilder out = new StringBuilder();
567            out.append(mAccount.serialize()).append(ACCOUNT_ENTRY_COMPONENT_SEPARATOR);
568            final String accountQueryUri =
569                    mAccountsQueryUri != null ? mAccountsQueryUri.toString() : "";
570            out.append(accountQueryUri);
571            return out.toString();
572        }
573
574        /**
575         * Create an account cache object from a serialized string previously stored away.
576         * If the serializedString does not parse as a valid account, we throw an
577         * {@link IllegalArgumentException}. The caller is responsible for checking this and
578         * ignoring the newly created object if the exception is thrown.
579         * @param serializedString
580         */
581        public AccountCacheEntry(String serializedString) throws IllegalArgumentException {
582            String[] cacheEntryMembers = TextUtils.split(serializedString,
583                    ACCOUNT_ENTRY_COMPONENT_SEPARATOR_PATTERN);
584            if (cacheEntryMembers.length != NUMBER_MEMBERS) {
585                throw new IllegalArgumentException("AccountCacheEntry de-serializing failed. "
586                        + "Wrong number of members detected. "
587                        + cacheEntryMembers.length + " detected");
588            }
589            mAccount = Account.newinstance(cacheEntryMembers[0]);
590            if (mAccount == null) {
591                throw new IllegalArgumentException("AccountCacheEntry de-serializing failed. "
592                        + "Account object could not be created from the serialized string: "
593                        + serializedString);
594            }
595            if (mAccount.settings == Settings.EMPTY_SETTINGS) {
596                throw new IllegalArgumentException("AccountCacheEntry de-serializing failed. "
597                        + "Settings could not be created from the string: " + serializedString);
598            }
599            mAccountsQueryUri = !TextUtils.isEmpty(cacheEntryMembers[1]) ?
600                    Uri.parse(cacheEntryMembers[1]) : null;
601        }
602    }
603}
604