MailAppProvider.java revision bc14a6f8da73a563c27dd99dc433f8c301dfe8ed
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.ContentProviderClient;
22import android.content.ContentResolver;
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.CursorLoader;
26import android.content.Intent;
27import android.content.Loader;
28import android.content.Loader.OnLoadCompleteListener;
29import android.content.SharedPreferences;
30import android.database.Cursor;
31import android.database.MatrixCursor;
32import android.net.Uri;
33import android.os.Bundle;
34
35import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
36import com.android.mail.providers.protos.boot.AccountReceiver;
37import com.android.mail.utils.LogTag;
38import com.android.mail.utils.LogUtils;
39import com.android.mail.utils.MatrixCursorWithExtra;
40import com.google.common.collect.ImmutableList;
41import com.google.common.collect.ImmutableSet;
42import com.google.common.collect.Maps;
43import com.google.common.collect.Sets;
44
45import org.json.JSONArray;
46import org.json.JSONException;
47import org.json.JSONObject;
48
49import java.util.LinkedHashMap;
50import java.util.List;
51import java.util.Map;
52import java.util.Set;
53
54
55/**
56 * The Mail App provider allows email providers to register "accounts" and the UI has a single
57 * place to query for the list of accounts.
58 *
59 * During development this will allow new account types to be added, and allow them to be shown in
60 * the application.  For example, the mock accounts can be enabled/disabled.
61 * In the future, once other processes can add new accounts, this could allow other "mail"
62 * applications have their content appear within the application
63 */
64public abstract class MailAppProvider extends ContentProvider
65        implements OnLoadCompleteListener<Cursor>{
66
67    private static final String SHARED_PREFERENCES_NAME = "MailAppProvider";
68    private static final String ACCOUNT_LIST_KEY = "accountList";
69    private static final String LAST_VIEWED_ACCOUNT_KEY = "lastViewedAccount";
70    private static final String LAST_SENT_FROM_ACCOUNT_KEY = "lastSendFromAccount";
71
72    /**
73     * Extra used in the result from the activity launched by the intent specified
74     * by {@link #getNoAccountsIntent} to return the list of accounts.  The data
75     * specified by this extra key should be a ParcelableArray.
76     */
77    public static final String ADD_ACCOUNT_RESULT_ACCOUNTS_EXTRA = "addAccountResultAccounts";
78
79    private final static String LOG_TAG = LogTag.getLogTag();
80
81    private final LinkedHashMap<Uri, AccountCacheEntry> mAccountCache =
82            new LinkedHashMap<Uri, AccountCacheEntry>();
83
84    private final Map<Uri, CursorLoader> mCursorLoaderMap = Maps.newHashMap();
85
86    private ContentResolver mResolver;
87    private static String sAuthority;
88    private static MailAppProvider sInstance;
89    private final static Set<Uri> PENDING_ACCOUNT_URIS = Sets.newHashSet();
90
91    private volatile boolean mAccountsFullyLoaded = false;
92
93    private SharedPreferences mSharedPrefs;
94
95    /**
96     * Allows the implementing provider to specify the authority for this provider. Email and Gmail
97     * must specify different authorities.
98     */
99    protected abstract String getAuthority();
100
101    /**
102     * Authority for the suggestions provider. Email and Gmail must specify different authorities,
103     * much like the implementation of {@link #getAuthority()}.
104     * @return the suggestion authority associated with this provider.
105     */
106    public abstract String getSuggestionAuthority();
107
108    /**
109     * Allows the implementing provider to specify an intent that should be used in a call to
110     * {@link Context#startActivityForResult(android.content.Intent)} when the account provider
111     * doesn't return any accounts.
112     *
113     * The result from the {@link Activity} activity should include the list of accounts in
114     * the returned intent, in the
115
116     * @return Intent or null, if the provider doesn't specify a behavior when no accounts are
117     * specified.
118     */
119    protected abstract Intent getNoAccountsIntent(Context context);
120
121    /**
122     * The cursor returned from a call to {@link android.content.ContentResolver#query()} with this
123     * uri will return a cursor that with columns that are a subset of the columns specified
124     * in {@link UIProvider.ConversationColumns}
125     * The cursor returned by this query can return a {@link android.os.Bundle}
126     * from a call to {@link android.database.Cursor#getExtras()}.  This Bundle may have
127     * values with keys listed in {@link AccountCursorExtraKeys}
128     */
129    public static Uri getAccountsUri() {
130        return Uri.parse("content://" + sAuthority + "/");
131    }
132
133    public static MailAppProvider getInstance() {
134        return sInstance;
135    }
136
137    /** Default constructor */
138    protected MailAppProvider() {
139    }
140
141    @Override
142    public boolean onCreate() {
143        sAuthority = getAuthority();
144        mResolver = getContext().getContentResolver();
145
146        final Intent intent = new Intent(AccountReceiver.ACTION_PROVIDER_CREATED);
147        getContext().sendBroadcast(intent);
148
149        // Load the previously saved account list
150        loadCachedAccountList();
151
152        synchronized (PENDING_ACCOUNT_URIS) {
153            sInstance = this;
154
155            // Handle the case where addAccountsForUriAsync was called before
156            // this Provider instance was created
157            final Set<Uri> urisToQery = ImmutableSet.copyOf(PENDING_ACCOUNT_URIS);
158            PENDING_ACCOUNT_URIS.clear();
159            for (Uri accountQueryUri : urisToQery) {
160                addAccountsForUriAsync(accountQueryUri);
161            }
162        }
163
164        return true;
165    }
166
167    @Override
168    public void shutdown() {
169        synchronized (PENDING_ACCOUNT_URIS) {
170            sInstance = null;
171        }
172
173        for (CursorLoader loader : mCursorLoaderMap.values()) {
174            loader.stopLoading();
175        }
176        mCursorLoaderMap.clear();
177    }
178
179    @Override
180    public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs,
181            String sortOrder) {
182        // This content provider currently only supports one query (to return the list of accounts).
183        // No reason to check the uri.  Currently only checking the projections
184
185        // Validates and returns the projection that should be used.
186        final String[] resultProjection = UIProviderValidator.validateAccountProjection(projection);
187        final Bundle extras = new Bundle();
188        extras.putInt(AccountCursorExtraKeys.ACCOUNTS_LOADED, mAccountsFullyLoaded ? 1 : 0);
189
190        // Make a copy of the account cache
191        final List<AccountCacheEntry> accountList;
192        synchronized (mAccountCache) {
193            accountList = ImmutableList.copyOf(mAccountCache.values());
194        }
195
196        final MatrixCursor cursor =
197                new MatrixCursorWithExtra(resultProjection, accountList.size(), extras);
198
199        for (AccountCacheEntry accountEntry : accountList) {
200            final Account account = accountEntry.mAccount;
201            final MatrixCursor.RowBuilder builder = cursor.newRow();
202
203            for (final String columnName : resultProjection) {
204                final int column = UIProvider.getAccountColumn(columnName);
205                switch (column) {
206                    case UIProvider.ACCOUNT_ID_COLUMN:
207                        builder.add(Integer.valueOf(0));
208                        break;
209                    case UIProvider.ACCOUNT_NAME_COLUMN:
210                        builder.add(account.name);
211                        break;
212                    case UIProvider.ACCOUNT_PROVIDER_VERISON_COLUMN:
213                        // TODO fix this
214                        builder.add(Integer.valueOf(account.providerVersion));
215                        break;
216                    case UIProvider.ACCOUNT_URI_COLUMN:
217                        builder.add(account.uri);
218                        break;
219                    case UIProvider.ACCOUNT_CAPABILITIES_COLUMN:
220                        builder.add(Integer.valueOf(account.capabilities));
221                        break;
222                    case UIProvider.ACCOUNT_FOLDER_LIST_URI_COLUMN:
223                        builder.add(account.folderListUri);
224                        break;
225                    case UIProvider.ACCOUNT_FULL_FOLDER_LIST_URI_COLUMN:
226                        builder.add(account.fullFolderListUri);
227                        break;
228                    case UIProvider.ACCOUNT_SEARCH_URI_COLUMN:
229                        builder.add(account.searchUri);
230                        break;
231                    case UIProvider.ACCOUNT_FROM_ADDRESSES_COLUMN:
232                        builder.add(account.accountFromAddresses);
233                        break;
234                    case UIProvider.ACCOUNT_SAVE_DRAFT_URI_COLUMN:
235                        builder.add(account.saveDraftUri);
236                        break;
237                    case UIProvider.ACCOUNT_SEND_MESSAGE_URI_COLUMN:
238                        builder.add(account.sendMessageUri);
239                        break;
240                    case UIProvider.ACCOUNT_EXPUNGE_MESSAGE_URI_COLUMN:
241                        builder.add(account.expungeMessageUri);
242                        break;
243                    case UIProvider.ACCOUNT_UNDO_URI_COLUMN:
244                        builder.add(account.undoUri);
245                        break;
246                    case UIProvider.ACCOUNT_SETTINGS_INTENT_URI_COLUMN:
247                        builder.add(account.settingsIntentUri);
248                        break;
249                    case UIProvider.ACCOUNT_HELP_INTENT_URI_COLUMN:
250                        builder.add(account.helpIntentUri);
251                        break;
252                    case UIProvider.ACCOUNT_SEND_FEEDBACK_INTENT_URI_COLUMN:
253                        builder.add(account.sendFeedbackIntentUri);
254                        break;
255                    case UIProvider.ACCOUNT_REAUTHENTICATION_INTENT_URI_COLUMN:
256                        builder.add(account.reauthenticationIntentUri);
257                        break;
258                    case UIProvider.ACCOUNT_SYNC_STATUS_COLUMN:
259                        builder.add(Integer.valueOf(account.syncStatus));
260                        break;
261                    case UIProvider.ACCOUNT_COMPOSE_INTENT_URI_COLUMN:
262                        builder.add(account.composeIntentUri);
263                        break;
264                    case UIProvider.ACCOUNT_MIME_TYPE_COLUMN:
265                        builder.add(account.mimeType);
266                        break;
267                    case UIProvider.ACCOUNT_RECENT_FOLDER_LIST_URI_COLUMN:
268                        builder.add(account.recentFolderListUri);
269                        break;
270                    case UIProvider.ACCOUNT_DEFAULT_RECENT_FOLDER_LIST_URI_COLUMN:
271                        builder.add(account.defaultRecentFolderListUri);
272                        break;
273                    case UIProvider.ACCOUNT_MANUAL_SYNC_URI_COLUMN:
274                        builder.add(account.manualSyncUri);
275                        break;
276                    case UIProvider.ACCOUNT_VIEW_INTENT_PROXY_URI_COLUMN:
277                        builder.add(account.viewIntentProxyUri);
278                        break;
279                    case UIProvider.ACCOUNT_COOKIE_QUERY_URI_COLUMN:
280                        builder.add(account.accoutCookieQueryUri);
281                        break;
282                    case UIProvider.ACCOUNT_COLOR_COLUMN:
283                        builder.add(account.color);
284                        break;
285
286                    case UIProvider.ACCOUNT_SETTINGS_SIGNATURE_COLUMN:
287                        builder.add(account.settings.signature);
288                        break;
289                    case UIProvider.ACCOUNT_SETTINGS_AUTO_ADVANCE_COLUMN:
290                        builder.add(Integer.valueOf(account.settings.getAutoAdvanceSetting()));
291                        break;
292                    case UIProvider.ACCOUNT_SETTINGS_MESSAGE_TEXT_SIZE_COLUMN:
293                        builder.add(Integer.valueOf(account.settings.messageTextSize));
294                        break;
295                    case UIProvider.ACCOUNT_SETTINGS_REPLY_BEHAVIOR_COLUMN:
296                        builder.add(Integer.valueOf(account.settings.replyBehavior));
297                        break;
298                    case UIProvider.ACCOUNT_SETTINGS_HIDE_CHECKBOXES_COLUMN:
299                        builder.add(Integer.valueOf(account.settings.hideCheckboxes ? 1 : 0));
300                        break;
301                    case UIProvider.ACCOUNT_SETTINGS_CONFIRM_DELETE_COLUMN:
302                        builder.add(Integer.valueOf(account.settings.confirmDelete ? 1 : 0));
303                        break;
304                    case UIProvider.ACCOUNT_SETTINGS_CONFIRM_ARCHIVE_COLUMN:
305                        builder.add(Integer.valueOf(account.settings.confirmArchive ? 1 : 0));
306                        break;
307                    case UIProvider.ACCOUNT_SETTINGS_CONFIRM_SEND_COLUMN:
308                        builder.add(Integer.valueOf(account.settings.confirmSend ? 1 : 0));
309                        break;
310                    case UIProvider.ACCOUNT_SETTINGS_DEFAULT_INBOX_COLUMN:
311                        builder.add(account.settings.defaultInbox);
312                        break;
313                    case UIProvider.ACCOUNT_SETTINGS_DEFAULT_INBOX_NAME_COLUMN:
314                        builder.add(account.settings.defaultInboxName);
315                        break;
316                    case UIProvider.ACCOUNT_SETTINGS_SNAP_HEADERS_COLUMN:
317                        builder.add(Integer.valueOf(account.settings.snapHeaders));
318                        break;
319                    case UIProvider.ACCOUNT_SETTINGS_FORCE_REPLY_FROM_DEFAULT_COLUMN:
320                        builder.add(Integer.valueOf(account.settings.forceReplyFromDefault ? 1 : 0));
321                        break;
322                    case UIProvider.ACCOUNT_SETTINGS_MAX_ATTACHMENT_SIZE_COLUMN:
323                        builder.add(account.settings.maxAttachmentSize);
324                        break;
325                    case UIProvider.ACCOUNT_SETTINGS_SWIPE_COLUMN:
326                        builder.add(account.settings.swipe);
327                        break;
328                    case UIProvider.ACCOUNT_SETTINGS_PRIORITY_ARROWS_ENABLED_COLUMN:
329                        builder.add(Integer.valueOf(account.settings.priorityArrowsEnabled ? 1 : 0));
330                        break;
331                    case UIProvider.ACCOUNT_SETTINGS_SETUP_INTENT_URI:
332                        builder.add(account.settings.setupIntentUri);
333                        break;
334                    case UIProvider.ACCOUNT_SETTINGS_CONVERSATION_MODE_COLUMN:
335                        builder.add(account.settings.conversationViewMode);
336                        break;
337                    case UIProvider.ACCOUNT_UPDATE_SETTINGS_URI_COLUMN:
338                        builder.add(account.updateSettingsUri);
339                        break;
340                    default:
341                        throw new IllegalStateException("Column not found: " + columnName);
342                }
343            }
344        }
345
346        cursor.setNotificationUri(mResolver, getAccountsUri());
347        return cursor;
348    }
349
350    @Override
351    public Uri insert(Uri url, ContentValues values) {
352        return url;
353    }
354
355    @Override
356    public int update(Uri url, ContentValues values, String selection,
357            String[] selectionArgs) {
358        return 0;
359    }
360
361    @Override
362    public int delete(Uri url, String selection, String[] selectionArgs) {
363        return 0;
364    }
365
366    @Override
367    public String getType(Uri uri) {
368        return null;
369    }
370
371    /**
372     * Asynchronously adds all of the accounts that are specified by the result set returned by
373     * {@link ContentProvider#query()} for the specified uri.  The content provider handling the
374     * query needs to handle the {@link UIProvider.ACCOUNTS_PROJECTION}
375     * Any changes to the underlying provider will automatically be reflected.
376     * @param resolver
377     * @param accountsQueryUri
378     */
379    public static void addAccountsForUriAsync(Uri accountsQueryUri) {
380        synchronized (PENDING_ACCOUNT_URIS) {
381            final MailAppProvider instance = getInstance();
382            if (instance != null) {
383                instance.startAccountsLoader(accountsQueryUri);
384            } else {
385                PENDING_ACCOUNT_URIS.add(accountsQueryUri);
386            }
387        }
388    }
389
390    /**
391     * Returns the intent that should be used in a call to
392     * {@link Context#startActivity(android.content.Intent)} when the account provider doesn't
393     * return any accounts
394     * @return Intent or null, if the provider doesn't specify a behavior when no acccounts are
395     * specified.
396     */
397    public static Intent getNoAccountIntent(Context context) {
398        return getInstance().getNoAccountsIntent(context);
399    }
400
401    private synchronized void startAccountsLoader(Uri accountsQueryUri) {
402        final CursorLoader accountsCursorLoader = new CursorLoader(getContext(), accountsQueryUri,
403                UIProvider.ACCOUNTS_PROJECTION, null, null, null);
404
405        // Listen for the results
406        accountsCursorLoader.registerListener(accountsQueryUri.hashCode(), this);
407        accountsCursorLoader.startLoading();
408
409        // If there is a previous loader for the given uri, stop it
410        final CursorLoader oldLoader = mCursorLoaderMap.get(accountsQueryUri);
411        if (oldLoader != null) {
412            oldLoader.stopLoading();
413        }
414        mCursorLoaderMap.put(accountsQueryUri, accountsCursorLoader);
415    }
416
417    public static void addAccount(Account account, Uri accountsQueryUri) {
418        final MailAppProvider provider = getInstance();
419        if (provider == null) {
420            throw new IllegalStateException("MailAppProvider not intialized");
421        }
422        provider.addAccountImpl(account, accountsQueryUri, true /* notify */);
423
424        // Cache the updated account list
425        provider.cacheAccountList();
426    }
427
428    private void addAccountImpl(Account account, Uri accountsQueryUri, boolean notify) {
429        addAccountImpl(account.uri, new AccountCacheEntry(account, accountsQueryUri));
430
431        // Explicitly calling this out of the synchronized block in case any of the observers get
432        // called synchronously.
433        if (notify) {
434            broadcastAccountChange();
435        }
436    }
437
438    private void addAccountImpl(Uri key, AccountCacheEntry accountEntry) {
439        synchronized (mAccountCache) {
440            LogUtils.v(LOG_TAG, "adding account %s", accountEntry.mAccount);
441            // LinkedHashMap will not change the iteration order when re-inserting a key
442            mAccountCache.put(key, accountEntry);
443        }
444    }
445
446    private static void broadcastAccountChange() {
447        final MailAppProvider provider = sInstance;
448
449        if (provider != null) {
450            provider.mResolver.notifyChange(getAccountsUri(), null);
451        }
452    }
453
454    /**
455     * Returns the {@link Account#uri} (in String form) of the last viewed account.
456     */
457    public String getLastViewedAccount() {
458        return getPreferences().getString(LAST_VIEWED_ACCOUNT_KEY, null);
459    }
460
461    /**
462     * Persists the {@link Account#uri} (in String form) of the last viewed account.
463     */
464    public void setLastViewedAccount(String accountUriStr) {
465        final SharedPreferences.Editor editor = getPreferences().edit();
466        editor.putString(LAST_VIEWED_ACCOUNT_KEY, accountUriStr);
467        editor.apply();
468    }
469
470    /**
471     * Returns the {@link Account#uri} (in String form) of the last account the
472     * user compose a message from.
473     */
474    public String getLastSentFromAccount() {
475        return getPreferences().getString(LAST_SENT_FROM_ACCOUNT_KEY, null);
476    }
477
478    /**
479     * Persists the {@link Account#uri} (in String form) of the last account the
480     * user compose a message from.
481     */
482    public void setLastSentFromAccount(String accountUriStr) {
483        final SharedPreferences.Editor editor = getPreferences().edit();
484        editor.putString(LAST_SENT_FROM_ACCOUNT_KEY, accountUriStr);
485        editor.apply();
486    }
487
488    private void loadCachedAccountList() {
489        JSONArray accounts = null;
490        try {
491            final String accountsJson = getPreferences().getString(ACCOUNT_LIST_KEY, null);
492            if (accountsJson != null) {
493                accounts = new JSONArray(accountsJson);
494            }
495        } catch (Exception e) {
496            LogUtils.e(LOG_TAG, e, "ignoring unparsable accounts cache");
497        }
498
499        if (accounts == null) {
500            return;
501        }
502
503        for (int i = 0; i < accounts.length(); i++) {
504            try {
505                final AccountCacheEntry accountEntry = new AccountCacheEntry(
506                        accounts.getJSONObject(i));
507
508                if (accountEntry.mAccount.settings == null) {
509                    LogUtils.e(LOG_TAG, "Dropping account that doesn't specify settings");
510                    continue;
511                }
512
513                Account account = accountEntry.mAccount;
514                ContentProviderClient client =
515                        mResolver.acquireContentProviderClient(account.uri);
516                if (client != null) {
517                    client.release();
518                    addAccountImpl(account.uri, accountEntry);
519                } else {
520                    LogUtils.e(LOG_TAG, "Dropping account without provider: %s",
521                            account.name);
522                }
523
524            } catch (Exception e) {
525                // Unable to create account object, skip to next
526                LogUtils.e(LOG_TAG, e,
527                        "Unable to create account object from serialized form");
528            }
529        }
530        broadcastAccountChange();
531    }
532
533    private void cacheAccountList() {
534        final List<AccountCacheEntry> accountList;
535
536        synchronized (mAccountCache) {
537            accountList = ImmutableList.copyOf(mAccountCache.values());
538        }
539
540        final JSONArray arr = new JSONArray();
541        for (AccountCacheEntry accountEntry : accountList) {
542            arr.put(accountEntry.toJSONObject());
543        }
544
545        final SharedPreferences.Editor editor = getPreferences().edit();
546        editor.putString(ACCOUNT_LIST_KEY, arr.toString());
547        editor.apply();
548    }
549
550    private SharedPreferences getPreferences() {
551        if (mSharedPrefs == null) {
552            mSharedPrefs = getContext().getSharedPreferences(
553                    SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
554        }
555        return mSharedPrefs;
556    }
557
558    static public Account getAccountFromAccountUri(Uri accountUri) {
559        MailAppProvider provider = getInstance();
560        if (provider != null && provider.mAccountsFullyLoaded) {
561            synchronized(provider.mAccountCache) {
562                AccountCacheEntry entry = provider.mAccountCache.get(accountUri);
563                if (entry != null) {
564                    return entry.mAccount;
565                }
566            }
567        }
568        return null;
569    }
570
571    @Override
572    public void onLoadComplete(Loader<Cursor> loader, Cursor data) {
573        if (data == null) {
574            LogUtils.d(LOG_TAG, "null account cursor returned");
575            return;
576        }
577
578        LogUtils.d(LOG_TAG, "Cursor with %d accounts returned", data.getCount());
579        final CursorLoader cursorLoader = (CursorLoader)loader;
580        final Uri accountsQueryUri = cursorLoader.getUri();
581
582        // preserve ordering on partial updates
583        // also preserve ordering on complete updates for any that existed previously
584
585
586        final List<AccountCacheEntry> accountList;
587        synchronized (mAccountCache) {
588            accountList = ImmutableList.copyOf(mAccountCache.values());
589        }
590
591        // Build a set of the account uris that had been associated with that query
592        final Set<Uri> previousQueryUriSet = Sets.newHashSet();
593        for (AccountCacheEntry entry : accountList) {
594            if (accountsQueryUri.equals(entry.mAccountsQueryUri)) {
595                previousQueryUriSet.add(entry.mAccount.uri);
596            }
597        }
598
599        // Update the internal state of this provider if the returned result set
600        // represents all accounts
601        // TODO: determine what should happen with a heterogeneous set of accounts
602        final Bundle extra = data.getExtras();
603        mAccountsFullyLoaded = extra.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
604
605        final Set<Uri> newQueryUriMap = Sets.newHashSet();
606
607        // We are relying on the fact that all accounts are added in the order specified in the
608        // cursor.  Initially assume that we insert these items to at the end of the list
609        while (data.moveToNext()) {
610            final Account account = new Account(data);
611            final Uri accountUri = account.uri;
612            newQueryUriMap.add(accountUri);
613            // preserve existing order if already present and this is a partial update,
614            // otherwise add to the end
615            //
616            // N.B. this ordering policy means the order in which providers respond will affect
617            // the order of accounts.
618            if (mAccountsFullyLoaded) {
619                synchronized (mAccountCache) {
620                    // removing the existing item will prevent LinkedHashMap from preserving the
621                    // original insertion order
622                    mAccountCache.remove(accountUri);
623                }
624            }
625            addAccountImpl(account, accountsQueryUri, false /* don't notify */);
626        }
627        // Remove all of the accounts that are in the new result set
628        previousQueryUriSet.removeAll(newQueryUriMap);
629
630        // For all of the entries that had been in the previous result set, and are not
631        // in the new result set, remove them from the cache
632        if (previousQueryUriSet.size() > 0 && mAccountsFullyLoaded) {
633            synchronized (mAccountCache) {
634                for (Uri accountUri : previousQueryUriSet) {
635                    LogUtils.d(LOG_TAG, "Removing account %s", accountUri);
636                    mAccountCache.remove(accountUri);
637                }
638            }
639        }
640        broadcastAccountChange();
641
642        // Cache the updated account list
643        cacheAccountList();
644    }
645
646    /**
647     * Object that allows the Account Cache provider to associate the account with the content
648     * provider uri that originated that account.
649     */
650    private static class AccountCacheEntry {
651        final Account mAccount;
652        final Uri mAccountsQueryUri;
653
654        private static final String KEY_ACCOUNT = "acct";
655        private static final String KEY_QUERY_URI = "queryUri";
656
657        public AccountCacheEntry(Account account, Uri accountQueryUri) {
658            mAccount = account;
659            mAccountsQueryUri = accountQueryUri;
660        }
661
662        public AccountCacheEntry(JSONObject o) throws JSONException {
663            mAccount = Account.newinstance(o.getString(KEY_ACCOUNT));
664            if (mAccount == null) {
665                throw new IllegalArgumentException("AccountCacheEntry de-serializing failed. "
666                        + "Account object could not be created from the JSONObject: "
667                        + o);
668            }
669            if (mAccount.settings == Settings.EMPTY_SETTINGS) {
670                throw new IllegalArgumentException("AccountCacheEntry de-serializing failed. "
671                        + "Settings could not be created from the JSONObject: " + o);
672            }
673            final String uriStr = o.optString(KEY_QUERY_URI, null);
674            if (uriStr != null) {
675                mAccountsQueryUri = Uri.parse(uriStr);
676            } else {
677                mAccountsQueryUri = null;
678            }
679        }
680
681        public JSONObject toJSONObject() {
682            try {
683                return new JSONObject()
684                .put(KEY_ACCOUNT, mAccount.serialize())
685                .putOpt(KEY_QUERY_URI, mAccountsQueryUri);
686            } catch (JSONException e) {
687                // shouldn't happen
688                throw new IllegalArgumentException(e);
689            }
690        }
691
692    }
693}
694