Home | History | Annotate | Download | only in providers
      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 
     17 package com.android.mail.providers;
     18 
     19 import android.app.Activity;
     20 import android.content.ContentProvider;
     21 import android.content.ContentProviderClient;
     22 import android.content.ContentResolver;
     23 import android.content.ContentValues;
     24 import android.content.Context;
     25 import android.content.CursorLoader;
     26 import android.content.Intent;
     27 import android.content.Loader;
     28 import android.content.Loader.OnLoadCompleteListener;
     29 import android.content.SharedPreferences;
     30 import android.content.res.Resources;
     31 import android.database.Cursor;
     32 import android.database.MatrixCursor;
     33 import android.net.Uri;
     34 import android.os.Bundle;
     35 
     36 import com.android.mail.R;
     37 import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
     38 import com.android.mail.utils.LogTag;
     39 import com.android.mail.utils.LogUtils;
     40 import com.android.mail.utils.MatrixCursorWithExtra;
     41 import com.google.common.collect.ImmutableList;
     42 import com.google.common.collect.Maps;
     43 import com.google.common.collect.Sets;
     44 
     45 import org.json.JSONArray;
     46 import org.json.JSONException;
     47 import org.json.JSONObject;
     48 
     49 import java.util.LinkedHashMap;
     50 import java.util.List;
     51 import java.util.Map;
     52 import 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  */
     64 public 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 
     90     private volatile boolean mAccountsFullyLoaded = false;
     91 
     92     private SharedPreferences mSharedPrefs;
     93 
     94     /**
     95      * Allows the implementing provider to specify the authority for this provider. Email and Gmail
     96      * must specify different authorities.
     97      */
     98     protected abstract String getAuthority();
     99 
    100     /**
    101      * Authority for the suggestions provider. Email and Gmail must specify different authorities,
    102      * much like the implementation of {@link #getAuthority()}.
    103      * @return the suggestion authority associated with this provider.
    104      */
    105     public abstract String getSuggestionAuthority();
    106 
    107     /**
    108      * Allows the implementing provider to specify an intent that should be used in a call to
    109      * {@link Context#startActivityForResult(android.content.Intent)} when the account provider
    110      * doesn't return any accounts.
    111      *
    112      * The result from the {@link Activity} activity should include the list of accounts in
    113      * the returned intent, in the
    114 
    115      * @return Intent or null, if the provider doesn't specify a behavior when no accounts are
    116      * specified.
    117      */
    118     protected abstract Intent getNoAccountsIntent(Context context);
    119 
    120     /**
    121      * The cursor returned from a call to {@link android.content.ContentResolver#query()} with this
    122      * uri will return a cursor that with columns that are a subset of the columns specified
    123      * in {@link UIProvider.ConversationColumns}
    124      * The cursor returned by this query can return a {@link android.os.Bundle}
    125      * from a call to {@link android.database.Cursor#getExtras()}.  This Bundle may have
    126      * values with keys listed in {@link AccountCursorExtraKeys}
    127      */
    128     public static Uri getAccountsUri() {
    129         return Uri.parse("content://" + sAuthority + "/");
    130     }
    131 
    132     public static MailAppProvider getInstance() {
    133         return sInstance;
    134     }
    135 
    136     /** Default constructor */
    137     protected MailAppProvider() {
    138     }
    139 
    140     @Override
    141     public boolean onCreate() {
    142         sAuthority = getAuthority();
    143         sInstance = this;
    144         mResolver = getContext().getContentResolver();
    145 
    146         // Load the previously saved account list
    147         loadCachedAccountList();
    148 
    149         final Resources res = getContext().getResources();
    150         // Load the uris for the account list
    151         final String[] accountQueryUris = res.getStringArray(R.array.account_providers);
    152 
    153         for (String accountQueryUri : accountQueryUris) {
    154             final Uri uri = Uri.parse(accountQueryUri);
    155             addAccountsForUriAsync(uri);
    156         }
    157 
    158         return true;
    159     }
    160 
    161     @Override
    162     public void shutdown() {
    163         sInstance = null;
    164 
    165         for (CursorLoader loader : mCursorLoaderMap.values()) {
    166             loader.stopLoading();
    167         }
    168         mCursorLoaderMap.clear();
    169     }
    170 
    171     @Override
    172     public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs,
    173             String sortOrder) {
    174         // This content provider currently only supports one query (to return the list of accounts).
    175         // No reason to check the uri.  Currently only checking the projections
    176 
    177         // Validates and returns the projection that should be used.
    178         final String[] resultProjection = UIProviderValidator.validateAccountProjection(projection);
    179         final Bundle extras = new Bundle();
    180         extras.putInt(AccountCursorExtraKeys.ACCOUNTS_LOADED, mAccountsFullyLoaded ? 1 : 0);
    181 
    182         // Make a copy of the account cache
    183         final List<AccountCacheEntry> accountList;
    184         synchronized (mAccountCache) {
    185             accountList = ImmutableList.copyOf(mAccountCache.values());
    186         }
    187 
    188         final MatrixCursor cursor =
    189                 new MatrixCursorWithExtra(resultProjection, accountList.size(), extras);
    190 
    191         for (AccountCacheEntry accountEntry : accountList) {
    192             final Account account = accountEntry.mAccount;
    193             final MatrixCursor.RowBuilder builder = cursor.newRow();
    194             final Map<String, Object> accountValues = account.getValueMap();
    195 
    196             for (final String columnName : resultProjection) {
    197                 if (accountValues.containsKey(columnName)) {
    198                     builder.add(accountValues.get(columnName));
    199                 } else {
    200                     throw new IllegalStateException("Unexpected column: " + columnName);
    201                 }
    202             }
    203         }
    204 
    205         cursor.setNotificationUri(mResolver, getAccountsUri());
    206         return cursor;
    207     }
    208 
    209     @Override
    210     public Uri insert(Uri url, ContentValues values) {
    211         return url;
    212     }
    213 
    214     @Override
    215     public int update(Uri url, ContentValues values, String selection,
    216             String[] selectionArgs) {
    217         return 0;
    218     }
    219 
    220     @Override
    221     public int delete(Uri url, String selection, String[] selectionArgs) {
    222         return 0;
    223     }
    224 
    225     @Override
    226     public String getType(Uri uri) {
    227         return null;
    228     }
    229 
    230     /**
    231      * Asynchronously adds all of the accounts that are specified by the result set returned by
    232      * {@link ContentProvider#query()} for the specified uri.  The content provider handling the
    233      * query needs to handle the {@link UIProvider.ACCOUNTS_PROJECTION}
    234      * Any changes to the underlying provider will automatically be reflected.
    235      * @param accountsQueryUri
    236      */
    237     private void addAccountsForUriAsync(Uri accountsQueryUri) {
    238         startAccountsLoader(accountsQueryUri);
    239     }
    240 
    241     /**
    242      * Returns the intent that should be used in a call to
    243      * {@link Context#startActivity(android.content.Intent)} when the account provider doesn't
    244      * return any accounts
    245      * @return Intent or null, if the provider doesn't specify a behavior when no acccounts are
    246      * specified.
    247      */
    248     public static Intent getNoAccountIntent(Context context) {
    249         return getInstance().getNoAccountsIntent(context);
    250     }
    251 
    252     private synchronized void startAccountsLoader(Uri accountsQueryUri) {
    253         final CursorLoader accountsCursorLoader = new CursorLoader(getContext(), accountsQueryUri,
    254                 UIProvider.ACCOUNTS_PROJECTION, null, null, null);
    255 
    256         // Listen for the results
    257         accountsCursorLoader.registerListener(accountsQueryUri.hashCode(), this);
    258         accountsCursorLoader.startLoading();
    259 
    260         // If there is a previous loader for the given uri, stop it
    261         final CursorLoader oldLoader = mCursorLoaderMap.get(accountsQueryUri);
    262         if (oldLoader != null) {
    263             oldLoader.stopLoading();
    264         }
    265         mCursorLoaderMap.put(accountsQueryUri, accountsCursorLoader);
    266     }
    267 
    268     private void addAccountImpl(Account account, Uri accountsQueryUri, boolean notify) {
    269         addAccountImpl(account.uri, new AccountCacheEntry(account, accountsQueryUri));
    270 
    271         // Explicitly calling this out of the synchronized block in case any of the observers get
    272         // called synchronously.
    273         if (notify) {
    274             broadcastAccountChange();
    275         }
    276     }
    277 
    278     private void addAccountImpl(Uri key, AccountCacheEntry accountEntry) {
    279         synchronized (mAccountCache) {
    280             LogUtils.v(LOG_TAG, "adding account %s", accountEntry.mAccount);
    281             // LinkedHashMap will not change the iteration order when re-inserting a key
    282             mAccountCache.put(key, accountEntry);
    283         }
    284     }
    285 
    286     private static void broadcastAccountChange() {
    287         final MailAppProvider provider = sInstance;
    288 
    289         if (provider != null) {
    290             provider.mResolver.notifyChange(getAccountsUri(), null);
    291         }
    292     }
    293 
    294     /**
    295      * Returns the {@link Account#uri} (in String form) of the last viewed account.
    296      */
    297     public String getLastViewedAccount() {
    298         return getPreferences().getString(LAST_VIEWED_ACCOUNT_KEY, null);
    299     }
    300 
    301     /**
    302      * Persists the {@link Account#uri} (in String form) of the last viewed account.
    303      */
    304     public void setLastViewedAccount(String accountUriStr) {
    305         final SharedPreferences.Editor editor = getPreferences().edit();
    306         editor.putString(LAST_VIEWED_ACCOUNT_KEY, accountUriStr);
    307         editor.apply();
    308     }
    309 
    310     /**
    311      * Returns the {@link Account#uri} (in String form) of the last account the
    312      * user compose a message from.
    313      */
    314     public String getLastSentFromAccount() {
    315         return getPreferences().getString(LAST_SENT_FROM_ACCOUNT_KEY, null);
    316     }
    317 
    318     /**
    319      * Persists the {@link Account#uri} (in String form) of the last account the
    320      * user compose a message from.
    321      */
    322     public void setLastSentFromAccount(String accountUriStr) {
    323         final SharedPreferences.Editor editor = getPreferences().edit();
    324         editor.putString(LAST_SENT_FROM_ACCOUNT_KEY, accountUriStr);
    325         editor.apply();
    326     }
    327 
    328     private void loadCachedAccountList() {
    329         JSONArray accounts = null;
    330         try {
    331             final String accountsJson = getPreferences().getString(ACCOUNT_LIST_KEY, null);
    332             if (accountsJson != null) {
    333                 accounts = new JSONArray(accountsJson);
    334             }
    335         } catch (Exception e) {
    336             LogUtils.e(LOG_TAG, e, "ignoring unparsable accounts cache");
    337         }
    338 
    339         if (accounts == null) {
    340             return;
    341         }
    342 
    343         for (int i = 0; i < accounts.length(); i++) {
    344             try {
    345                 final AccountCacheEntry accountEntry = new AccountCacheEntry(
    346                         accounts.getJSONObject(i));
    347 
    348                 if (accountEntry.mAccount.settings == null) {
    349                     LogUtils.e(LOG_TAG, "Dropping account that doesn't specify settings");
    350                     continue;
    351                 }
    352 
    353                 Account account = accountEntry.mAccount;
    354                 ContentProviderClient client =
    355                         mResolver.acquireContentProviderClient(account.uri);
    356                 if (client != null) {
    357                     client.release();
    358                     addAccountImpl(account.uri, accountEntry);
    359                 } else {
    360                     LogUtils.e(LOG_TAG, "Dropping account without provider: %s",
    361                             account.name);
    362                 }
    363 
    364             } catch (Exception e) {
    365                 // Unable to create account object, skip to next
    366                 LogUtils.e(LOG_TAG, e,
    367                         "Unable to create account object from serialized form");
    368             }
    369         }
    370         broadcastAccountChange();
    371     }
    372 
    373     private void cacheAccountList() {
    374         final List<AccountCacheEntry> accountList;
    375 
    376         synchronized (mAccountCache) {
    377             accountList = ImmutableList.copyOf(mAccountCache.values());
    378         }
    379 
    380         final JSONArray arr = new JSONArray();
    381         for (AccountCacheEntry accountEntry : accountList) {
    382             arr.put(accountEntry.toJSONObject());
    383         }
    384 
    385         final SharedPreferences.Editor editor = getPreferences().edit();
    386         editor.putString(ACCOUNT_LIST_KEY, arr.toString());
    387         editor.apply();
    388     }
    389 
    390     private SharedPreferences getPreferences() {
    391         if (mSharedPrefs == null) {
    392             mSharedPrefs = getContext().getSharedPreferences(
    393                     SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
    394         }
    395         return mSharedPrefs;
    396     }
    397 
    398     static public Account getAccountFromAccountUri(Uri accountUri) {
    399         MailAppProvider provider = getInstance();
    400         if (provider != null && provider.mAccountsFullyLoaded) {
    401             synchronized(provider.mAccountCache) {
    402                 AccountCacheEntry entry = provider.mAccountCache.get(accountUri);
    403                 if (entry != null) {
    404                     return entry.mAccount;
    405                 }
    406             }
    407         }
    408         return null;
    409     }
    410 
    411     @Override
    412     public void onLoadComplete(Loader<Cursor> loader, Cursor data) {
    413         if (data == null) {
    414             LogUtils.d(LOG_TAG, "null account cursor returned");
    415             return;
    416         }
    417 
    418         LogUtils.d(LOG_TAG, "Cursor with %d accounts returned", data.getCount());
    419         final CursorLoader cursorLoader = (CursorLoader)loader;
    420         final Uri accountsQueryUri = cursorLoader.getUri();
    421 
    422         // preserve ordering on partial updates
    423         // also preserve ordering on complete updates for any that existed previously
    424 
    425 
    426         final List<AccountCacheEntry> accountList;
    427         synchronized (mAccountCache) {
    428             accountList = ImmutableList.copyOf(mAccountCache.values());
    429         }
    430 
    431         // Build a set of the account uris that had been associated with that query
    432         final Set<Uri> previousQueryUriSet = Sets.newHashSet();
    433         for (AccountCacheEntry entry : accountList) {
    434             if (accountsQueryUri.equals(entry.mAccountsQueryUri)) {
    435                 previousQueryUriSet.add(entry.mAccount.uri);
    436             }
    437         }
    438 
    439         // Update the internal state of this provider if the returned result set
    440         // represents all accounts
    441         // TODO: determine what should happen with a heterogeneous set of accounts
    442         final Bundle extra = data.getExtras();
    443         mAccountsFullyLoaded = extra.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0;
    444 
    445         final Set<Uri> newQueryUriMap = Sets.newHashSet();
    446 
    447         // We are relying on the fact that all accounts are added in the order specified in the
    448         // cursor.  Initially assume that we insert these items to at the end of the list
    449         while (data.moveToNext()) {
    450             final Account account = new Account(data);
    451             final Uri accountUri = account.uri;
    452             newQueryUriMap.add(accountUri);
    453             // preserve existing order if already present and this is a partial update,
    454             // otherwise add to the end
    455             //
    456             // N.B. this ordering policy means the order in which providers respond will affect
    457             // the order of accounts.
    458             if (mAccountsFullyLoaded) {
    459                 synchronized (mAccountCache) {
    460                     // removing the existing item will prevent LinkedHashMap from preserving the
    461                     // original insertion order
    462                     mAccountCache.remove(accountUri);
    463                 }
    464             }
    465             addAccountImpl(account, accountsQueryUri, false /* don't notify */);
    466         }
    467         // Remove all of the accounts that are in the new result set
    468         previousQueryUriSet.removeAll(newQueryUriMap);
    469 
    470         // For all of the entries that had been in the previous result set, and are not
    471         // in the new result set, remove them from the cache
    472         if (previousQueryUriSet.size() > 0 && mAccountsFullyLoaded) {
    473             synchronized (mAccountCache) {
    474                 for (Uri accountUri : previousQueryUriSet) {
    475                     LogUtils.d(LOG_TAG, "Removing account %s", accountUri);
    476                     mAccountCache.remove(accountUri);
    477                 }
    478             }
    479         }
    480         broadcastAccountChange();
    481 
    482         // Cache the updated account list
    483         cacheAccountList();
    484     }
    485 
    486     /**
    487      * Object that allows the Account Cache provider to associate the account with the content
    488      * provider uri that originated that account.
    489      */
    490     private static class AccountCacheEntry {
    491         final Account mAccount;
    492         final Uri mAccountsQueryUri;
    493 
    494         private static final String KEY_ACCOUNT = "acct";
    495         private static final String KEY_QUERY_URI = "queryUri";
    496 
    497         public AccountCacheEntry(Account account, Uri accountQueryUri) {
    498             mAccount = account;
    499             mAccountsQueryUri = accountQueryUri;
    500         }
    501 
    502         public AccountCacheEntry(JSONObject o) throws JSONException {
    503             mAccount = Account.newinstance(o.getString(KEY_ACCOUNT));
    504             if (mAccount == null) {
    505                 throw new IllegalArgumentException("AccountCacheEntry de-serializing failed. "
    506                         + "Account object could not be created from the JSONObject: "
    507                         + o);
    508             }
    509             if (mAccount.settings == Settings.EMPTY_SETTINGS) {
    510                 throw new IllegalArgumentException("AccountCacheEntry de-serializing failed. "
    511                         + "Settings could not be created from the JSONObject: " + o);
    512             }
    513             final String uriStr = o.optString(KEY_QUERY_URI, null);
    514             if (uriStr != null) {
    515                 mAccountsQueryUri = Uri.parse(uriStr);
    516             } else {
    517                 mAccountsQueryUri = null;
    518             }
    519         }
    520 
    521         public JSONObject toJSONObject() {
    522             try {
    523                 return new JSONObject()
    524                 .put(KEY_ACCOUNT, mAccount.serialize())
    525                 .putOpt(KEY_QUERY_URI, mAccountsQueryUri);
    526             } catch (JSONException e) {
    527                 // shouldn't happen
    528                 throw new IllegalArgumentException(e);
    529             }
    530         }
    531 
    532     }
    533 }
    534