Home | History | Annotate | Download | only in provider
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      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.email.provider;
     18 
     19 import android.accounts.AccountManager;
     20 import android.appwidget.AppWidgetManager;
     21 import android.content.ComponentCallbacks;
     22 import android.content.ComponentName;
     23 import android.content.ContentProvider;
     24 import android.content.ContentProviderOperation;
     25 import android.content.ContentProviderResult;
     26 import android.content.ContentResolver;
     27 import android.content.ContentUris;
     28 import android.content.ContentValues;
     29 import android.content.Context;
     30 import android.content.Intent;
     31 import android.content.OperationApplicationException;
     32 import android.content.PeriodicSync;
     33 import android.content.SharedPreferences;
     34 import android.content.UriMatcher;
     35 import android.content.pm.ActivityInfo;
     36 import android.content.pm.PackageManager;
     37 import android.content.res.Configuration;
     38 import android.content.res.Resources;
     39 import android.database.ContentObserver;
     40 import android.database.Cursor;
     41 import android.database.CursorWrapper;
     42 import android.database.DatabaseUtils;
     43 import android.database.MatrixCursor;
     44 import android.database.MergeCursor;
     45 import android.database.sqlite.SQLiteDatabase;
     46 import android.database.sqlite.SQLiteException;
     47 import android.database.sqlite.SQLiteStatement;
     48 import android.net.Uri;
     49 import android.os.AsyncTask;
     50 import android.os.Binder;
     51 import android.os.Build;
     52 import android.os.Bundle;
     53 import android.os.Handler;
     54 import android.os.Handler.Callback;
     55 import android.os.Looper;
     56 import android.os.Parcel;
     57 import android.os.ParcelFileDescriptor;
     58 import android.os.RemoteException;
     59 import android.provider.BaseColumns;
     60 import android.text.TextUtils;
     61 import android.text.format.DateUtils;
     62 import android.util.Base64;
     63 import android.util.Log;
     64 import android.util.SparseArray;
     65 
     66 import com.android.common.content.ProjectionMap;
     67 import com.android.email.DebugUtils;
     68 import com.android.email.NotificationController;
     69 import com.android.email.NotificationControllerCreatorHolder;
     70 import com.android.email.Preferences;
     71 import com.android.email.R;
     72 import com.android.email.SecurityPolicy;
     73 import com.android.email.activity.setup.AccountSecurity;
     74 import com.android.email.activity.setup.AccountSettingsUtils;
     75 import com.android.email.service.AttachmentService;
     76 import com.android.email.service.EmailServiceUtils;
     77 import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
     78 import com.android.emailcommon.Logging;
     79 import com.android.emailcommon.mail.Address;
     80 import com.android.emailcommon.provider.Account;
     81 import com.android.emailcommon.provider.Credential;
     82 import com.android.emailcommon.provider.EmailContent;
     83 import com.android.emailcommon.provider.EmailContent.AccountColumns;
     84 import com.android.emailcommon.provider.EmailContent.Attachment;
     85 import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
     86 import com.android.emailcommon.provider.EmailContent.Body;
     87 import com.android.emailcommon.provider.EmailContent.BodyColumns;
     88 import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
     89 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
     90 import com.android.emailcommon.provider.EmailContent.Message;
     91 import com.android.emailcommon.provider.EmailContent.MessageColumns;
     92 import com.android.emailcommon.provider.EmailContent.PolicyColumns;
     93 import com.android.emailcommon.provider.EmailContent.QuickResponseColumns;
     94 import com.android.emailcommon.provider.EmailContent.SyncColumns;
     95 import com.android.emailcommon.provider.HostAuth;
     96 import com.android.emailcommon.provider.Mailbox;
     97 import com.android.emailcommon.provider.MailboxUtilities;
     98 import com.android.emailcommon.provider.MessageChangeLogTable;
     99 import com.android.emailcommon.provider.MessageMove;
    100 import com.android.emailcommon.provider.MessageStateChange;
    101 import com.android.emailcommon.provider.Policy;
    102 import com.android.emailcommon.provider.QuickResponse;
    103 import com.android.emailcommon.service.EmailServiceProxy;
    104 import com.android.emailcommon.service.EmailServiceStatus;
    105 import com.android.emailcommon.service.IEmailService;
    106 import com.android.emailcommon.service.SearchParams;
    107 import com.android.emailcommon.utility.AttachmentUtilities;
    108 import com.android.emailcommon.utility.EmailAsyncTask;
    109 import com.android.emailcommon.utility.IntentUtilities;
    110 import com.android.emailcommon.utility.Utility;
    111 import com.android.ex.photo.provider.PhotoContract;
    112 import com.android.mail.preferences.MailPrefs;
    113 import com.android.mail.preferences.MailPrefs.PreferenceKeys;
    114 import com.android.mail.providers.Folder;
    115 import com.android.mail.providers.FolderList;
    116 import com.android.mail.providers.Settings;
    117 import com.android.mail.providers.UIProvider;
    118 import com.android.mail.providers.UIProvider.AccountCapabilities;
    119 import com.android.mail.providers.UIProvider.AccountColumns.SettingsColumns;
    120 import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
    121 import com.android.mail.providers.UIProvider.ConversationPriority;
    122 import com.android.mail.providers.UIProvider.ConversationSendingState;
    123 import com.android.mail.providers.UIProvider.DraftType;
    124 import com.android.mail.utils.AttachmentUtils;
    125 import com.android.mail.utils.LogTag;
    126 import com.android.mail.utils.LogUtils;
    127 import com.android.mail.utils.MatrixCursorWithCachedColumns;
    128 import com.android.mail.utils.MatrixCursorWithExtra;
    129 import com.android.mail.utils.MimeType;
    130 import com.android.mail.utils.Utils;
    131 import com.android.mail.widget.BaseWidgetProvider;
    132 import com.google.common.collect.ImmutableMap;
    133 import com.google.common.collect.ImmutableSet;
    134 import com.google.common.collect.Sets;
    135 
    136 import java.io.File;
    137 import java.io.FileDescriptor;
    138 import java.io.FileNotFoundException;
    139 import java.io.FileWriter;
    140 import java.io.IOException;
    141 import java.io.PrintWriter;
    142 import java.util.ArrayList;
    143 import java.util.Arrays;
    144 import java.util.Collection;
    145 import java.util.HashSet;
    146 import java.util.List;
    147 import java.util.Locale;
    148 import java.util.Map;
    149 import java.util.Set;
    150 import java.util.regex.Pattern;
    151 
    152 public class EmailProvider extends ContentProvider
    153         implements SharedPreferences.OnSharedPreferenceChangeListener {
    154 
    155     private static final String TAG = LogTag.getLogTag();
    156 
    157     // Time to delay upsync requests.
    158     public static final long SYNC_DELAY_MILLIS = 30 * DateUtils.SECOND_IN_MILLIS;
    159 
    160     public static String EMAIL_APP_MIME_TYPE;
    161 
    162     // exposed for testing
    163     public static final String DATABASE_NAME = "EmailProvider.db";
    164     public static final String BODY_DATABASE_NAME = "EmailProviderBody.db";
    165 
    166     // We don't back up to the backup database anymore, just keep this constant here so we can
    167     // delete the old backups and trigger a new backup to the account manager
    168     @Deprecated
    169     private static final String BACKUP_DATABASE_NAME = "EmailProviderBackup.db";
    170     private static final String ACCOUNT_MANAGER_JSON_TAG = "accountJson";
    171 
    172 
    173     private static final String PREFERENCE_FRAGMENT_CLASS_NAME =
    174             "com.android.email.activity.setup.AccountSettingsFragment";
    175     /**
    176      * Notifies that changes happened. Certain UI components, e.g., widgets, can register for this
    177      * {@link android.content.Intent} and update accordingly. However, this can be very broad and
    178      * is NOT the preferred way of getting notification.
    179      */
    180     private static final String ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED =
    181         "com.android.email.MESSAGE_LIST_DATASET_CHANGED";
    182 
    183     private static final String EMAIL_MESSAGE_MIME_TYPE =
    184         "vnd.android.cursor.item/email-message";
    185     private static final String EMAIL_ATTACHMENT_MIME_TYPE =
    186         "vnd.android.cursor.item/email-attachment";
    187 
    188     /** Appended to the notification URI for delete operations */
    189     private static final String NOTIFICATION_OP_DELETE = "delete";
    190     /** Appended to the notification URI for insert operations */
    191     private static final String NOTIFICATION_OP_INSERT = "insert";
    192     /** Appended to the notification URI for update operations */
    193     private static final String NOTIFICATION_OP_UPDATE = "update";
    194 
    195     /** The query string to trigger a folder refresh. */
    196     protected static String QUERY_UIREFRESH = "uirefresh";
    197 
    198     // Definitions for our queries looking for orphaned messages
    199     private static final String[] ORPHANS_PROJECTION
    200         = new String[] {MessageColumns._ID, MessageColumns.MAILBOX_KEY};
    201     private static final int ORPHANS_ID = 0;
    202     private static final int ORPHANS_MAILBOX_KEY = 1;
    203 
    204     private static final String WHERE_ID = BaseColumns._ID + "=?";
    205 
    206     private static final int ACCOUNT_BASE = 0;
    207     private static final int ACCOUNT = ACCOUNT_BASE;
    208     private static final int ACCOUNT_ID = ACCOUNT_BASE + 1;
    209     private static final int ACCOUNT_CHECK = ACCOUNT_BASE + 2;
    210     private static final int ACCOUNT_PICK_TRASH_FOLDER = ACCOUNT_BASE + 3;
    211     private static final int ACCOUNT_PICK_SENT_FOLDER = ACCOUNT_BASE + 4;
    212 
    213     private static final int MAILBOX_BASE = 0x1000;
    214     private static final int MAILBOX = MAILBOX_BASE;
    215     private static final int MAILBOX_ID = MAILBOX_BASE + 1;
    216     private static final int MAILBOX_NOTIFICATION = MAILBOX_BASE + 2;
    217     private static final int MAILBOX_MOST_RECENT_MESSAGE = MAILBOX_BASE + 3;
    218     private static final int MAILBOX_MESSAGE_COUNT = MAILBOX_BASE + 4;
    219 
    220     private static final int MESSAGE_BASE = 0x2000;
    221     private static final int MESSAGE = MESSAGE_BASE;
    222     private static final int MESSAGE_ID = MESSAGE_BASE + 1;
    223     private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2;
    224     private static final int MESSAGE_SELECTION = MESSAGE_BASE + 3;
    225     private static final int MESSAGE_MOVE = MESSAGE_BASE + 4;
    226     private static final int MESSAGE_STATE_CHANGE = MESSAGE_BASE + 5;
    227 
    228     private static final int ATTACHMENT_BASE = 0x3000;
    229     private static final int ATTACHMENT = ATTACHMENT_BASE;
    230     private static final int ATTACHMENT_ID = ATTACHMENT_BASE + 1;
    231     private static final int ATTACHMENTS_MESSAGE_ID = ATTACHMENT_BASE + 2;
    232     private static final int ATTACHMENTS_CACHED_FILE_ACCESS = ATTACHMENT_BASE + 3;
    233 
    234     private static final int HOSTAUTH_BASE = 0x4000;
    235     private static final int HOSTAUTH = HOSTAUTH_BASE;
    236     private static final int HOSTAUTH_ID = HOSTAUTH_BASE + 1;
    237 
    238     private static final int UPDATED_MESSAGE_BASE = 0x5000;
    239     private static final int UPDATED_MESSAGE = UPDATED_MESSAGE_BASE;
    240     private static final int UPDATED_MESSAGE_ID = UPDATED_MESSAGE_BASE + 1;
    241 
    242     private static final int DELETED_MESSAGE_BASE = 0x6000;
    243     private static final int DELETED_MESSAGE = DELETED_MESSAGE_BASE;
    244     private static final int DELETED_MESSAGE_ID = DELETED_MESSAGE_BASE + 1;
    245 
    246     private static final int POLICY_BASE = 0x7000;
    247     private static final int POLICY = POLICY_BASE;
    248     private static final int POLICY_ID = POLICY_BASE + 1;
    249 
    250     private static final int QUICK_RESPONSE_BASE = 0x8000;
    251     private static final int QUICK_RESPONSE = QUICK_RESPONSE_BASE;
    252     private static final int QUICK_RESPONSE_ID = QUICK_RESPONSE_BASE + 1;
    253     private static final int QUICK_RESPONSE_ACCOUNT_ID = QUICK_RESPONSE_BASE + 2;
    254 
    255     private static final int UI_BASE = 0x9000;
    256     private static final int UI_FOLDERS = UI_BASE;
    257     private static final int UI_SUBFOLDERS = UI_BASE + 1;
    258     private static final int UI_MESSAGES = UI_BASE + 2;
    259     private static final int UI_MESSAGE = UI_BASE + 3;
    260     private static final int UI_UNDO = UI_BASE + 4;
    261     private static final int UI_FOLDER_REFRESH = UI_BASE + 5;
    262     private static final int UI_FOLDER = UI_BASE + 6;
    263     private static final int UI_ACCOUNT = UI_BASE + 7;
    264     private static final int UI_ACCTS = UI_BASE + 8;
    265     private static final int UI_ATTACHMENTS = UI_BASE + 9;
    266     private static final int UI_ATTACHMENT = UI_BASE + 10;
    267     private static final int UI_ATTACHMENT_BY_CID = UI_BASE + 11;
    268     private static final int UI_SEARCH = UI_BASE + 12;
    269     private static final int UI_ACCOUNT_DATA = UI_BASE + 13;
    270     private static final int UI_FOLDER_LOAD_MORE = UI_BASE + 14;
    271     private static final int UI_CONVERSATION = UI_BASE + 15;
    272     private static final int UI_RECENT_FOLDERS = UI_BASE + 16;
    273     private static final int UI_DEFAULT_RECENT_FOLDERS = UI_BASE + 17;
    274     private static final int UI_FULL_FOLDERS = UI_BASE + 18;
    275     private static final int UI_ALL_FOLDERS = UI_BASE + 19;
    276     private static final int UI_PURGE_FOLDER = UI_BASE + 20;
    277     private static final int UI_INBOX = UI_BASE + 21;
    278     private static final int UI_ACCTSETTINGS = UI_BASE + 22;
    279 
    280     private static final int BODY_BASE = 0xA000;
    281     private static final int BODY = BODY_BASE;
    282     private static final int BODY_ID = BODY_BASE + 1;
    283     private static final int BODY_HTML = BODY_BASE + 2;
    284     private static final int BODY_TEXT = BODY_BASE + 3;
    285 
    286     private static final int CREDENTIAL_BASE = 0xB000;
    287     private static final int CREDENTIAL = CREDENTIAL_BASE;
    288     private static final int CREDENTIAL_ID = CREDENTIAL_BASE + 1;
    289 
    290     private static final int BASE_SHIFT = 12;  // 12 bits to the base type: 0, 0x1000, 0x2000, etc.
    291 
    292     private static final SparseArray<String> TABLE_NAMES;
    293     static {
    294         SparseArray<String> array = new SparseArray<String>(11);
    295         array.put(ACCOUNT_BASE >> BASE_SHIFT, Account.TABLE_NAME);
    296         array.put(MAILBOX_BASE >> BASE_SHIFT, Mailbox.TABLE_NAME);
    297         array.put(MESSAGE_BASE >> BASE_SHIFT, Message.TABLE_NAME);
    298         array.put(ATTACHMENT_BASE >> BASE_SHIFT, Attachment.TABLE_NAME);
    299         array.put(HOSTAUTH_BASE >> BASE_SHIFT, HostAuth.TABLE_NAME);
    300         array.put(UPDATED_MESSAGE_BASE >> BASE_SHIFT, Message.UPDATED_TABLE_NAME);
    301         array.put(DELETED_MESSAGE_BASE >> BASE_SHIFT, Message.DELETED_TABLE_NAME);
    302         array.put(POLICY_BASE >> BASE_SHIFT, Policy.TABLE_NAME);
    303         array.put(QUICK_RESPONSE_BASE >> BASE_SHIFT, QuickResponse.TABLE_NAME);
    304         array.put(UI_BASE >> BASE_SHIFT, null);
    305         array.put(BODY_BASE >> BASE_SHIFT, Body.TABLE_NAME);
    306         array.put(CREDENTIAL_BASE >> BASE_SHIFT, Credential.TABLE_NAME);
    307         TABLE_NAMES = array;
    308     }
    309 
    310     private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    311 
    312     /**
    313      * Functions which manipulate the database connection or files synchronize on this.
    314      * It's static because there can be multiple provider objects.
    315      * TODO: Do we actually need to synchronize across all DB access, not just connection creation?
    316      */
    317     private static final Object sDatabaseLock = new Object();
    318 
    319     /**
    320      * Let's only generate these SQL strings once, as they are used frequently
    321      * Note that this isn't relevant for table creation strings, since they are used only once
    322      */
    323     private static final String UPDATED_MESSAGE_INSERT = "insert or ignore into " +
    324         Message.UPDATED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " +
    325         BaseColumns._ID + '=';
    326 
    327     private static final String UPDATED_MESSAGE_DELETE = "delete from " +
    328         Message.UPDATED_TABLE_NAME + " where " + BaseColumns._ID + '=';
    329 
    330     private static final String DELETED_MESSAGE_INSERT = "insert or replace into " +
    331         Message.DELETED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " +
    332         BaseColumns._ID + '=';
    333 
    334     private static final String ORPHAN_BODY_MESSAGE_ID_SELECT =
    335             "select " + BodyColumns.MESSAGE_KEY + " from " + Body.TABLE_NAME +
    336                     " except select " + BaseColumns._ID + " from " + Message.TABLE_NAME;
    337 
    338     private static final String DELETE_ORPHAN_BODIES = "delete from " + Body.TABLE_NAME +
    339         " where " + BodyColumns.MESSAGE_KEY + " in " + '(' + ORPHAN_BODY_MESSAGE_ID_SELECT + ')';
    340 
    341     private static final String DELETE_BODY = "delete from " + Body.TABLE_NAME +
    342         " where " + BodyColumns.MESSAGE_KEY + '=';
    343 
    344     private static final ContentValues EMPTY_CONTENT_VALUES = new ContentValues();
    345 
    346     private static final String MESSAGE_URI_PARAMETER_MAILBOX_ID = "mailboxId";
    347 
    348     // For undo handling
    349     private int mLastSequence = -1;
    350     private final ArrayList<ContentProviderOperation> mLastSequenceOps =
    351             new ArrayList<ContentProviderOperation>();
    352 
    353     // Query parameter indicating the command came from UIProvider
    354     private static final String IS_UIPROVIDER = "is_uiprovider";
    355 
    356     private static final String SYNC_STATUS_CALLBACK_METHOD = "sync_status";
    357 
    358     private static final String[] MIME_TYPE_PROJECTION = new String[]{AttachmentColumns.MIME_TYPE};
    359 
    360     private static final String[] CACHED_FILE_QUERY_PROJECTION = new String[]
    361             { AttachmentColumns._ID, AttachmentColumns.FILENAME, AttachmentColumns.SIZE,
    362                     AttachmentColumns.CONTENT_URI };
    363 
    364     /**
    365      * Wrap the UriMatcher call so we can throw a runtime exception if an unknown Uri is passed in
    366      * @param uri the Uri to match
    367      * @return the match value
    368      */
    369     private static int findMatch(Uri uri, String methodName) {
    370         int match = sURIMatcher.match(uri);
    371         if (match < 0) {
    372             throw new IllegalArgumentException("Unknown uri: " + uri);
    373         } else if (Logging.LOGD) {
    374             LogUtils.v(TAG, methodName + ": uri=" + uri + ", match is " + match);
    375         }
    376         return match;
    377     }
    378 
    379     // exposed for testing
    380     public static Uri INTEGRITY_CHECK_URI;
    381 
    382     public static Uri ACCOUNT_BACKUP_URI;
    383     private static Uri FOLDER_STATUS_URI;
    384 
    385     private SQLiteDatabase mDatabase;
    386     private SQLiteDatabase mBodyDatabase;
    387 
    388     private Handler mDelayedSyncHandler;
    389     private final Set<SyncRequestMessage> mDelayedSyncRequests = new HashSet<SyncRequestMessage>();
    390 
    391     private static void reconcileAccountsAsync(final Context context) {
    392         if (context.getResources().getBoolean(R.bool.reconcile_accounts)) {
    393             EmailAsyncTask.runAsyncParallel(new Runnable() {
    394                 @Override
    395                 public void run() {
    396                     AccountReconciler.reconcileAccounts(context);
    397                 }
    398             });
    399         }
    400     }
    401 
    402     public static Uri uiUri(String type, long id) {
    403         return Uri.parse(uiUriString(type, id));
    404     }
    405 
    406     /**
    407      * Creates a URI string from a database ID (guaranteed to be unique).
    408      * @param type of the resource: uifolder, message, etc.
    409      * @param id the id of the resource.
    410      * @return uri string
    411      */
    412     public static String uiUriString(String type, long id) {
    413         return "content://" + EmailContent.AUTHORITY + "/" + type + ((id == -1) ? "" : ("/" + id));
    414     }
    415 
    416     /**
    417      * Orphan record deletion utility.  Generates a sqlite statement like:
    418      *  delete from <table> where <column> not in (select <foreignColumn> from <foreignTable>)
    419      * Exposed for testing.
    420      * @param db the EmailProvider database
    421      * @param table the table whose orphans are to be removed
    422      * @param column the column deletion will be based on
    423      * @param foreignColumn the column in the foreign table whose absence will trigger the deletion
    424      * @param foreignTable the foreign table
    425      */
    426     public static void deleteUnlinked(SQLiteDatabase db, String table, String column,
    427             String foreignColumn, String foreignTable) {
    428         int count = db.delete(table, column + " not in (select " + foreignColumn + " from " +
    429                 foreignTable + ")", null);
    430         if (count > 0) {
    431             LogUtils.w(TAG, "Found " + count + " orphaned row(s) in " + table);
    432         }
    433     }
    434 
    435 
    436     /**
    437      * Make sure that parentKeys match with parentServerId.
    438      * When we sync folders, we do two passes: First to create the mailbox rows, and second
    439      * to set the parentKeys. Two passes are needed because we won't know the parent's Id
    440      * until that row is inserted, and the order in which the rows are given is arbitrary.
    441      * If we crash while this operation is in progress, the parent keys can be left uninitialized.
    442      * @param db SQLiteDatabase to modify
    443      */
    444     private void fixParentKeys(SQLiteDatabase db) {
    445         LogUtils.d(TAG, "Fixing parent keys");
    446 
    447         // Update the parentKey for each mailbox row to match the _id of the row whose
    448         // serverId matches our parentServerId. This will leave parentKey blank for any
    449         // row that does not have a parentServerId
    450 
    451         // This is kind of a confusing sql statement, so here's the actual text of it,
    452         // for reference:
    453         //
    454         //   update mailbox set parentKey = (select _id from mailbox as b where
    455         //   mailbox.parentServerId=b.serverId and mailbox.parentServerId not null and
    456         //   mailbox.accountKey=b.accountKey)
    457         db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.PARENT_KEY + "="
    458                 + "(select " + Mailbox._ID + " from " + Mailbox.TABLE_NAME + " as b where "
    459                 + Mailbox.TABLE_NAME + "." + MailboxColumns.PARENT_SERVER_ID + "="
    460                 + "b." + MailboxColumns.SERVER_ID + " and "
    461                 + Mailbox.TABLE_NAME + "." + MailboxColumns.PARENT_SERVER_ID + " not null and "
    462                 + Mailbox.TABLE_NAME + "." + MailboxColumns.ACCOUNT_KEY
    463                 + "=b." + Mailbox.ACCOUNT_KEY + ")");
    464 
    465         // Top level folders can still have uninitialized parent keys. Update these
    466         // to indicate that the parent is -1.
    467         //
    468         //   update mailbox set parentKey = -1 where parentKey=0 or parentKey is null;
    469         db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.PARENT_KEY
    470                 + "=" + Mailbox.NO_MAILBOX + " where " + MailboxColumns.PARENT_KEY
    471                 + "=" + Mailbox.PARENT_KEY_UNINITIALIZED + " or " + MailboxColumns.PARENT_KEY
    472                 + " is null");
    473 
    474     }
    475 
    476     // exposed for testing
    477     public SQLiteDatabase getDatabase(Context context) {
    478         synchronized (sDatabaseLock) {
    479             // Always return the cached database, if we've got one
    480             if (mDatabase != null) {
    481                 return mDatabase;
    482             }
    483 
    484             // Whenever we create or re-cache the databases, make sure that we haven't lost one
    485             // to corruption
    486             checkDatabases();
    487 
    488             DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME);
    489             mDatabase = helper.getWritableDatabase();
    490             DBHelper.BodyDatabaseHelper bodyHelper =
    491                     new DBHelper.BodyDatabaseHelper(context, BODY_DATABASE_NAME);
    492             mBodyDatabase = bodyHelper.getWritableDatabase();
    493             if (mBodyDatabase != null) {
    494                 String bodyFileName = mBodyDatabase.getPath();
    495                 mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase");
    496             }
    497 
    498             // Restore accounts if the database is corrupted...
    499             restoreIfNeeded(context, mDatabase);
    500             // Check for any orphaned Messages in the updated/deleted tables
    501             deleteMessageOrphans(mDatabase, Message.UPDATED_TABLE_NAME);
    502             deleteMessageOrphans(mDatabase, Message.DELETED_TABLE_NAME);
    503             // Delete orphaned mailboxes/messages/policies (account no longer exists)
    504             deleteUnlinked(mDatabase, Mailbox.TABLE_NAME, MailboxColumns.ACCOUNT_KEY,
    505                     AccountColumns._ID, Account.TABLE_NAME);
    506             deleteUnlinked(mDatabase, Message.TABLE_NAME, MessageColumns.ACCOUNT_KEY,
    507                     AccountColumns._ID, Account.TABLE_NAME);
    508             deleteUnlinked(mDatabase, Policy.TABLE_NAME, PolicyColumns._ID,
    509                     AccountColumns.POLICY_KEY, Account.TABLE_NAME);
    510             fixParentKeys(mDatabase);
    511             initUiProvider();
    512             return mDatabase;
    513         }
    514     }
    515 
    516     /**
    517      * Perform startup actions related to UI
    518      */
    519     private void initUiProvider() {
    520         // Clear mailbox sync status
    521         mDatabase.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UI_SYNC_STATUS +
    522                 "=" + UIProvider.SyncStatus.NO_SYNC);
    523     }
    524 
    525     /**
    526      * Restore user Account and HostAuth data from our backup database
    527      */
    528     private static void restoreIfNeeded(Context context, SQLiteDatabase mainDatabase) {
    529         if (DebugUtils.DEBUG) {
    530             LogUtils.w(TAG, "restoreIfNeeded...");
    531         }
    532         // Check for legacy backup
    533         String legacyBackup = Preferences.getLegacyBackupPreference(context);
    534         // If there's a legacy backup, create a new-style backup and delete the legacy backup
    535         // In the 1:1000000000 chance that the user gets an app update just as his database becomes
    536         // corrupt, oh well...
    537         if (!TextUtils.isEmpty(legacyBackup)) {
    538             backupAccounts(context, mainDatabase);
    539             Preferences.clearLegacyBackupPreference(context);
    540             LogUtils.w(TAG, "Created new EmailProvider backup database");
    541             return;
    542         }
    543 
    544         // If there's a backup database (old style) delete it and trigger an account manager backup.
    545         // Roughly the same comment as above applies
    546         final File backupDb = context.getDatabasePath(BACKUP_DATABASE_NAME);
    547         if (backupDb.exists()) {
    548             backupAccounts(context, mainDatabase);
    549             context.deleteDatabase(BACKUP_DATABASE_NAME);
    550             LogUtils.w(TAG, "Migrated from backup database to account manager");
    551             return;
    552         }
    553 
    554         // If we have accounts, we're done
    555         if (DatabaseUtils.longForQuery(mainDatabase,
    556                                       "SELECT EXISTS (SELECT ? FROM " + Account.TABLE_NAME + " )",
    557                                       EmailContent.ID_PROJECTION) > 0) {
    558             if (DebugUtils.DEBUG) {
    559                 LogUtils.w(TAG, "restoreIfNeeded: Account exists.");
    560             }
    561             return;
    562         }
    563 
    564         restoreAccounts(context);
    565     }
    566 
    567     /** {@inheritDoc} */
    568     @Override
    569     public void shutdown() {
    570         if (mDatabase != null) {
    571             mDatabase.close();
    572             mDatabase = null;
    573         }
    574         if (mBodyDatabase != null) {
    575             mBodyDatabase.close();
    576             mBodyDatabase = null;
    577         }
    578     }
    579 
    580     // exposed for testing
    581     public static void deleteMessageOrphans(SQLiteDatabase database, String tableName) {
    582         if (database != null) {
    583             // We'll look at all of the items in the table; there won't be many typically
    584             Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null);
    585             // Usually, there will be nothing in these tables, so make a quick check
    586             try {
    587                 if (c.getCount() == 0) return;
    588                 ArrayList<Long> foundMailboxes = new ArrayList<Long>();
    589                 ArrayList<Long> notFoundMailboxes = new ArrayList<Long>();
    590                 ArrayList<Long> deleteList = new ArrayList<Long>();
    591                 String[] bindArray = new String[1];
    592                 while (c.moveToNext()) {
    593                     // Get the mailbox key and see if we've already found this mailbox
    594                     // If so, we're fine
    595                     long mailboxId = c.getLong(ORPHANS_MAILBOX_KEY);
    596                     // If we already know this mailbox doesn't exist, mark the message for deletion
    597                     if (notFoundMailboxes.contains(mailboxId)) {
    598                         deleteList.add(c.getLong(ORPHANS_ID));
    599                     // If we don't know about this mailbox, we'll try to find it
    600                     } else if (!foundMailboxes.contains(mailboxId)) {
    601                         bindArray[0] = Long.toString(mailboxId);
    602                         Cursor boxCursor = database.query(Mailbox.TABLE_NAME,
    603                                 Mailbox.ID_PROJECTION, WHERE_ID, bindArray, null, null, null);
    604                         try {
    605                             // If it exists, we'll add it to the "found" mailboxes
    606                             if (boxCursor.moveToFirst()) {
    607                                 foundMailboxes.add(mailboxId);
    608                             // Otherwise, we'll add to "not found" and mark the message for deletion
    609                             } else {
    610                                 notFoundMailboxes.add(mailboxId);
    611                                 deleteList.add(c.getLong(ORPHANS_ID));
    612                             }
    613                         } finally {
    614                             boxCursor.close();
    615                         }
    616                     }
    617                 }
    618                 // Now, delete the orphan messages
    619                 for (long messageId: deleteList) {
    620                     bindArray[0] = Long.toString(messageId);
    621                     database.delete(tableName, WHERE_ID, bindArray);
    622                 }
    623             } finally {
    624                 c.close();
    625             }
    626         }
    627     }
    628 
    629     @Override
    630     public int delete(Uri uri, String selection, String[] selectionArgs) {
    631         Log.d(TAG, "Delete: " + uri);
    632         final int match = findMatch(uri, "delete");
    633         final Context context = getContext();
    634         // Pick the correct database for this operation
    635         // If we're in a transaction already (which would happen during applyBatch), then the
    636         // body database is already attached to the email database and any attempt to use the
    637         // body database directly will result in a SQLiteException (the database is locked)
    638         final SQLiteDatabase db = getDatabase(context);
    639         final int table = match >> BASE_SHIFT;
    640         String id = "0";
    641         boolean messageDeletion = false;
    642 
    643         final String tableName = TABLE_NAMES.valueAt(table);
    644         int result = -1;
    645 
    646         try {
    647             if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) {
    648                 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
    649                     notifyUIConversation(uri);
    650                 }
    651             }
    652             switch (match) {
    653                 case UI_MESSAGE:
    654                     return uiDeleteMessage(uri);
    655                 case UI_ACCOUNT_DATA:
    656                     return uiDeleteAccountData(uri);
    657                 case UI_ACCOUNT:
    658                     return uiDeleteAccount(uri);
    659                 case UI_PURGE_FOLDER:
    660                     return uiPurgeFolder(uri);
    661                 case MESSAGE_SELECTION:
    662                     Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection,
    663                             selectionArgs, null, null, null);
    664                     try {
    665                         if (findCursor.moveToFirst()) {
    666                             return delete(ContentUris.withAppendedId(
    667                                     Message.CONTENT_URI,
    668                                     findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)),
    669                                     null, null);
    670                         } else {
    671                             return 0;
    672                         }
    673                     } finally {
    674                         findCursor.close();
    675                     }
    676                 // These are cases in which one or more Messages might get deleted, either by
    677                 // cascade or explicitly
    678                 case MAILBOX_ID:
    679                 case MAILBOX:
    680                 case ACCOUNT_ID:
    681                 case ACCOUNT:
    682                 case MESSAGE:
    683                 case SYNCED_MESSAGE_ID:
    684                 case MESSAGE_ID:
    685                     // Handle lost Body records here, since this cannot be done in a trigger
    686                     // The process is:
    687                     //  1) Begin a transaction, ensuring that both databases are affected atomically
    688                     //  2) Do the requested deletion, with cascading deletions handled in triggers
    689                     //  3) End the transaction, committing all changes atomically
    690                     //
    691                     // Bodies are auto-deleted here;  Attachments are auto-deleted via trigger
    692                     messageDeletion = true;
    693                     db.beginTransaction();
    694                     break;
    695             }
    696             switch (match) {
    697                 case BODY_ID:
    698                 case DELETED_MESSAGE_ID:
    699                 case SYNCED_MESSAGE_ID:
    700                 case MESSAGE_ID:
    701                 case UPDATED_MESSAGE_ID:
    702                 case ATTACHMENT_ID:
    703                 case MAILBOX_ID:
    704                 case ACCOUNT_ID:
    705                 case HOSTAUTH_ID:
    706                 case POLICY_ID:
    707                 case QUICK_RESPONSE_ID:
    708                 case CREDENTIAL_ID:
    709                     id = uri.getPathSegments().get(1);
    710                     if (match == SYNCED_MESSAGE_ID) {
    711                         // For synced messages, first copy the old message to the deleted table and
    712                         // delete it from the updated table (in case it was updated first)
    713                         // Note that this is all within a transaction, for atomicity
    714                         db.execSQL(DELETED_MESSAGE_INSERT + id);
    715                         db.execSQL(UPDATED_MESSAGE_DELETE + id);
    716                     }
    717 
    718                     final long accountId;
    719                     if (match == MAILBOX_ID) {
    720                         accountId = Mailbox.getAccountIdForMailbox(context, id);
    721                     } else {
    722                         accountId = Account.NO_ACCOUNT;
    723                     }
    724 
    725                     result = db.delete(tableName, whereWithId(id, selection), selectionArgs);
    726 
    727                     if (match == ACCOUNT_ID) {
    728                         notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id);
    729                         notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null);
    730                     } else if (match == MAILBOX_ID) {
    731                         notifyUIFolder(id, accountId);
    732                     } else if (match == ATTACHMENT_ID) {
    733                         notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id);
    734                     }
    735                     break;
    736                 case ATTACHMENTS_MESSAGE_ID:
    737                     // All attachments for the given message
    738                     id = uri.getPathSegments().get(2);
    739                     result = db.delete(tableName,
    740                             whereWith(AttachmentColumns.MESSAGE_KEY + "=" + id, selection),
    741                             selectionArgs);
    742                     break;
    743 
    744                 case BODY:
    745                 case MESSAGE:
    746                 case DELETED_MESSAGE:
    747                 case UPDATED_MESSAGE:
    748                 case ATTACHMENT:
    749                 case MAILBOX:
    750                 case ACCOUNT:
    751                 case HOSTAUTH:
    752                 case POLICY:
    753                     result = db.delete(tableName, selection, selectionArgs);
    754                     break;
    755                 case MESSAGE_MOVE:
    756                     db.delete(MessageMove.TABLE_NAME, selection, selectionArgs);
    757                     break;
    758                 case MESSAGE_STATE_CHANGE:
    759                     db.delete(MessageStateChange.TABLE_NAME, selection, selectionArgs);
    760                     break;
    761                 default:
    762                     throw new IllegalArgumentException("Unknown URI " + uri);
    763             }
    764             if (messageDeletion) {
    765                 if (match == MESSAGE_ID) {
    766                     // Delete the Body record associated with the deleted message
    767                     final long messageId = Long.valueOf(id);
    768                     try {
    769                         deleteBodyFiles(context, messageId);
    770                     } catch (final IllegalStateException e) {
    771                         LogUtils.v(LogUtils.TAG, e, "Exception while deleting bodies");
    772                     }
    773                     db.execSQL(DELETE_BODY + id);
    774                 } else {
    775                     // Delete any orphaned Body records
    776                     final Cursor orphans = db.rawQuery(ORPHAN_BODY_MESSAGE_ID_SELECT, null);
    777                     try {
    778                         while (orphans.moveToNext()) {
    779                             final long messageId = orphans.getLong(0);
    780                             try {
    781                                 deleteBodyFiles(context, messageId);
    782                             } catch (final IllegalStateException e) {
    783                                 LogUtils.v(LogUtils.TAG, e, "Exception while deleting bodies");
    784                             }
    785                         }
    786                     } finally {
    787                         orphans.close();
    788                     }
    789                     db.execSQL(DELETE_ORPHAN_BODIES);
    790                 }
    791                 db.setTransactionSuccessful();
    792             }
    793         } catch (SQLiteException e) {
    794             checkDatabases();
    795             throw e;
    796         } finally {
    797             if (messageDeletion) {
    798                 db.endTransaction();
    799             }
    800         }
    801 
    802         // Notify all notifier cursors
    803         sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_DELETE, id);
    804 
    805         // Notify all email content cursors
    806         notifyUI(EmailContent.CONTENT_URI, null);
    807         return result;
    808     }
    809 
    810     @Override
    811     // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM)
    812     public String getType(Uri uri) {
    813         int match = findMatch(uri, "getType");
    814         switch (match) {
    815             case BODY_ID:
    816                 return "vnd.android.cursor.item/email-body";
    817             case BODY:
    818                 return "vnd.android.cursor.dir/email-body";
    819             case UPDATED_MESSAGE_ID:
    820             case MESSAGE_ID:
    821                 // NOTE: According to the framework folks, we're supposed to invent mime types as
    822                 // a way of passing information to drag & drop recipients.
    823                 // If there's a mailboxId parameter in the url, we respond with a mime type that
    824                 // has -n appended, where n is the mailboxId of the message.  The drag & drop code
    825                 // uses this information to know not to allow dragging the item to its own mailbox
    826                 String mimeType = EMAIL_MESSAGE_MIME_TYPE;
    827                 String mailboxId = uri.getQueryParameter(MESSAGE_URI_PARAMETER_MAILBOX_ID);
    828                 if (mailboxId != null) {
    829                     mimeType += "-" + mailboxId;
    830                 }
    831                 return mimeType;
    832             case UPDATED_MESSAGE:
    833             case MESSAGE:
    834                 return "vnd.android.cursor.dir/email-message";
    835             case MAILBOX:
    836                 return "vnd.android.cursor.dir/email-mailbox";
    837             case MAILBOX_ID:
    838                 return "vnd.android.cursor.item/email-mailbox";
    839             case ACCOUNT:
    840                 return "vnd.android.cursor.dir/email-account";
    841             case ACCOUNT_ID:
    842                 return "vnd.android.cursor.item/email-account";
    843             case ATTACHMENTS_MESSAGE_ID:
    844             case ATTACHMENT:
    845                 return "vnd.android.cursor.dir/email-attachment";
    846             case ATTACHMENT_ID:
    847                 return EMAIL_ATTACHMENT_MIME_TYPE;
    848             case HOSTAUTH:
    849                 return "vnd.android.cursor.dir/email-hostauth";
    850             case HOSTAUTH_ID:
    851                 return "vnd.android.cursor.item/email-hostauth";
    852             case ATTACHMENTS_CACHED_FILE_ACCESS: {
    853                 SQLiteDatabase db = getDatabase(getContext());
    854                 Cursor c = db.query(Attachment.TABLE_NAME, MIME_TYPE_PROJECTION,
    855                         AttachmentColumns.CACHED_FILE + "=?", new String[]{uri.toString()},
    856                         null, null, null, null);
    857                 try {
    858                     if (c != null && c.moveToFirst()) {
    859                         return c.getString(0);
    860                     } else {
    861                         return null;
    862                     }
    863                 } finally {
    864                     if (c != null) {
    865                         c.close();
    866                     }
    867                 }
    868             }
    869             default:
    870                 return null;
    871         }
    872     }
    873 
    874     // These URIs are used for specific UI notifications. We don't use EmailContent.CONTENT_URI
    875     // as the base because that gets spammed.
    876     // These can't be statically initialized because they depend on EmailContent.AUTHORITY
    877     private static Uri UIPROVIDER_CONVERSATION_NOTIFIER;
    878     private static Uri UIPROVIDER_FOLDER_NOTIFIER;
    879     private static Uri UIPROVIDER_FOLDERLIST_NOTIFIER;
    880     private static Uri UIPROVIDER_ACCOUNT_NOTIFIER;
    881     // Not currently used
    882     //public static Uri UIPROVIDER_SETTINGS_NOTIFIER;
    883     private static Uri UIPROVIDER_ATTACHMENT_NOTIFIER;
    884     private static Uri UIPROVIDER_ATTACHMENTS_NOTIFIER;
    885     private static Uri UIPROVIDER_ALL_ACCOUNTS_NOTIFIER;
    886     private static Uri UIPROVIDER_MESSAGE_NOTIFIER;
    887     private static Uri UIPROVIDER_RECENT_FOLDERS_NOTIFIER;
    888 
    889     @Override
    890     public Uri insert(Uri uri, ContentValues values) {
    891         Log.d(TAG, "Insert: " + uri);
    892         final int match = findMatch(uri, "insert");
    893         final Context context = getContext();
    894 
    895         // See the comment at delete(), above
    896         final SQLiteDatabase db = getDatabase(context);
    897         final int table = match >> BASE_SHIFT;
    898         String id = "0";
    899         long longId;
    900 
    901         // We do NOT allow setting of unreadCount/messageCount via the provider
    902         // These columns are maintained via triggers
    903         if (match == MAILBOX_ID || match == MAILBOX) {
    904             values.put(MailboxColumns.UNREAD_COUNT, 0);
    905             values.put(MailboxColumns.MESSAGE_COUNT, 0);
    906         }
    907 
    908         final Uri resultUri;
    909 
    910         try {
    911             switch (match) {
    912                 case BODY:
    913                     final ContentValues dbValues = new ContentValues(values);
    914                     // Prune out the content we don't want in the DB
    915                     dbValues.remove(BodyColumns.HTML_CONTENT);
    916                     dbValues.remove(BodyColumns.TEXT_CONTENT);
    917                     // TODO: move this to the message table
    918                     longId = db.insert(Body.TABLE_NAME, "foo", dbValues);
    919                     resultUri = ContentUris.withAppendedId(uri, longId);
    920                     // Write content to the filesystem where appropriate
    921                     // This will look less ugly once the body table is folded into the message table
    922                     // and we can just use longId instead
    923                     if (!values.containsKey(BodyColumns.MESSAGE_KEY)) {
    924                         throw new IllegalArgumentException(
    925                                 "Cannot insert body without MESSAGE_KEY");
    926                     }
    927                     final long messageId = values.getAsLong(BodyColumns.MESSAGE_KEY);
    928                     // Ensure that no pre-existing body files contaminate the message
    929                     deleteBodyFiles(context, messageId);
    930                     writeBodyFiles(getContext(), messageId, values);
    931                     break;
    932                 // NOTE: It is NOT legal for production code to insert directly into UPDATED_MESSAGE
    933                 // or DELETED_MESSAGE; see the comment below for details
    934                 case UPDATED_MESSAGE:
    935                 case DELETED_MESSAGE:
    936                 case MESSAGE:
    937                     decodeEmailAddresses(values);
    938                 case ATTACHMENT:
    939                 case MAILBOX:
    940                 case ACCOUNT:
    941                 case HOSTAUTH:
    942                 case CREDENTIAL:
    943                 case POLICY:
    944                 case QUICK_RESPONSE:
    945                     longId = db.insert(TABLE_NAMES.valueAt(table), "foo", values);
    946                     resultUri = ContentUris.withAppendedId(uri, longId);
    947                     switch(match) {
    948                         case MESSAGE:
    949                             final long mailboxId = values.getAsLong(MessageColumns.MAILBOX_KEY);
    950                             if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
    951                                 notifyUIConversationMailbox(mailboxId);
    952                             }
    953                             notifyUIFolder(mailboxId, values.getAsLong(MessageColumns.ACCOUNT_KEY));
    954                             break;
    955                         case MAILBOX:
    956                             if (values.containsKey(MailboxColumns.TYPE)) {
    957                                 if (values.getAsInteger(MailboxColumns.TYPE) <
    958                                         Mailbox.TYPE_NOT_EMAIL) {
    959                                     // Notify the account when a new mailbox is added
    960                                     final Long accountId =
    961                                             values.getAsLong(MailboxColumns.ACCOUNT_KEY);
    962                                     if (accountId != null && accountId > 0) {
    963                                         notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, accountId);
    964                                         notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId);
    965                                     }
    966                                 }
    967                             }
    968                             break;
    969                         case ACCOUNT:
    970                             updateAccountSyncInterval(longId, values);
    971                             if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
    972                                 notifyUIAccount(longId);
    973                             }
    974                             notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null);
    975                             break;
    976                         case UPDATED_MESSAGE:
    977                         case DELETED_MESSAGE:
    978                             throw new IllegalArgumentException("Unknown URL " + uri);
    979                         case ATTACHMENT:
    980                             int flags = 0;
    981                             if (values.containsKey(AttachmentColumns.FLAGS)) {
    982                                 flags = values.getAsInteger(AttachmentColumns.FLAGS);
    983                             }
    984                             // Report all new attachments to the download service
    985                             if (TextUtils.isEmpty(values.getAsString(AttachmentColumns.LOCATION))) {
    986                                 LogUtils.w(TAG, new Throwable(), "attachment with blank location");
    987                             }
    988                             mAttachmentService.attachmentChanged(getContext(), longId, flags);
    989                             break;
    990                     }
    991                     break;
    992                 case QUICK_RESPONSE_ACCOUNT_ID:
    993                     longId = Long.parseLong(uri.getPathSegments().get(2));
    994                     values.put(QuickResponseColumns.ACCOUNT_KEY, longId);
    995                     return insert(QuickResponse.CONTENT_URI, values);
    996                 case MAILBOX_ID:
    997                     // This implies adding a message to a mailbox
    998                     // Hmm, a problem here is that we can't link the account as well, so it must be
    999                     // already in the values...
   1000                     longId = Long.parseLong(uri.getPathSegments().get(1));
   1001                     values.put(MessageColumns.MAILBOX_KEY, longId);
   1002                     return insert(Message.CONTENT_URI, values); // Recurse
   1003                 case MESSAGE_ID:
   1004                     // This implies adding an attachment to a message.
   1005                     id = uri.getPathSegments().get(1);
   1006                     longId = Long.parseLong(id);
   1007                     values.put(AttachmentColumns.MESSAGE_KEY, longId);
   1008                     return insert(Attachment.CONTENT_URI, values); // Recurse
   1009                 case ACCOUNT_ID:
   1010                     // This implies adding a mailbox to an account.
   1011                     longId = Long.parseLong(uri.getPathSegments().get(1));
   1012                     values.put(MailboxColumns.ACCOUNT_KEY, longId);
   1013                     return insert(Mailbox.CONTENT_URI, values); // Recurse
   1014                 case ATTACHMENTS_MESSAGE_ID:
   1015                     longId = db.insert(TABLE_NAMES.valueAt(table), "foo", values);
   1016                     resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, longId);
   1017                     break;
   1018                 default:
   1019                     throw new IllegalArgumentException("Unknown URL " + uri);
   1020             }
   1021         } catch (SQLiteException e) {
   1022             checkDatabases();
   1023             throw e;
   1024         }
   1025 
   1026         // Notify all notifier cursors
   1027         sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_INSERT, id);
   1028 
   1029         // Notify all existing cursors.
   1030         notifyUI(EmailContent.CONTENT_URI, null);
   1031         return resultUri;
   1032     }
   1033 
   1034     @Override
   1035     public boolean onCreate() {
   1036         Context context = getContext();
   1037         EmailContent.init(context);
   1038         init(context);
   1039         DebugUtils.init(context);
   1040         // Do this last, so that EmailContent/EmailProvider are initialized
   1041         setServicesEnabledAsync(context);
   1042         reconcileAccountsAsync(context);
   1043 
   1044         // Update widgets
   1045         final Intent updateAllWidgetsIntent =
   1046                 new Intent(com.android.mail.utils.Utils.ACTION_NOTIFY_DATASET_CHANGED);
   1047         updateAllWidgetsIntent.putExtra(BaseWidgetProvider.EXTRA_UPDATE_ALL_WIDGETS, true);
   1048         updateAllWidgetsIntent.setType(context.getString(R.string.application_mime_type));
   1049         context.sendBroadcast(updateAllWidgetsIntent);
   1050 
   1051         // The combined account name changes on locale changes
   1052         final Configuration oldConfiguration =
   1053                 new Configuration(context.getResources().getConfiguration());
   1054         context.registerComponentCallbacks(new ComponentCallbacks() {
   1055             @Override
   1056             public void onConfigurationChanged(Configuration configuration) {
   1057                 int delta = oldConfiguration.updateFrom(configuration);
   1058                 if (Configuration.needNewResources(delta, ActivityInfo.CONFIG_LOCALE)) {
   1059                     notifyUIAccount(COMBINED_ACCOUNT_ID);
   1060                 }
   1061             }
   1062 
   1063             @Override
   1064             public void onLowMemory() {}
   1065         });
   1066 
   1067         MailPrefs.get(context).registerOnSharedPreferenceChangeListener(this);
   1068 
   1069         return false;
   1070     }
   1071 
   1072     private static void init(final Context context) {
   1073         // Synchronize on the matcher rather than the class object to minimize risk of contention
   1074         // & deadlock.
   1075         synchronized (sURIMatcher) {
   1076             // We use the existence of this variable as indicative of whether this function has
   1077             // already run.
   1078             if (INTEGRITY_CHECK_URI != null) {
   1079                 return;
   1080             }
   1081             INTEGRITY_CHECK_URI = Uri.parse("content://" + EmailContent.AUTHORITY +
   1082                     "/integrityCheck");
   1083             ACCOUNT_BACKUP_URI =
   1084                     Uri.parse("content://" + EmailContent.AUTHORITY + "/accountBackup");
   1085             FOLDER_STATUS_URI =
   1086                     Uri.parse("content://" + EmailContent.AUTHORITY + "/status");
   1087             EMAIL_APP_MIME_TYPE = context.getString(R.string.application_mime_type);
   1088 
   1089             final String uiNotificationAuthority =
   1090                     EmailContent.EMAIL_PACKAGE_NAME + ".uinotifications";
   1091             UIPROVIDER_CONVERSATION_NOTIFIER =
   1092                     Uri.parse("content://" + uiNotificationAuthority + "/uimessages");
   1093             UIPROVIDER_FOLDER_NOTIFIER =
   1094                     Uri.parse("content://" + uiNotificationAuthority + "/uifolder");
   1095             UIPROVIDER_FOLDERLIST_NOTIFIER =
   1096                     Uri.parse("content://" + uiNotificationAuthority + "/uifolders");
   1097             UIPROVIDER_ACCOUNT_NOTIFIER =
   1098                     Uri.parse("content://" + uiNotificationAuthority + "/uiaccount");
   1099             // Not currently used
   1100             /* UIPROVIDER_SETTINGS_NOTIFIER =
   1101                     Uri.parse("content://" + uiNotificationAuthority + "/uisettings");*/
   1102             UIPROVIDER_ATTACHMENT_NOTIFIER =
   1103                     Uri.parse("content://" + uiNotificationAuthority + "/uiattachment");
   1104             UIPROVIDER_ATTACHMENTS_NOTIFIER =
   1105                     Uri.parse("content://" + uiNotificationAuthority + "/uiattachments");
   1106             UIPROVIDER_ALL_ACCOUNTS_NOTIFIER =
   1107                     Uri.parse("content://" + uiNotificationAuthority + "/uiaccts");
   1108             UIPROVIDER_MESSAGE_NOTIFIER =
   1109                     Uri.parse("content://" + uiNotificationAuthority + "/uimessage");
   1110             UIPROVIDER_RECENT_FOLDERS_NOTIFIER =
   1111                     Uri.parse("content://" + uiNotificationAuthority + "/uirecentfolders");
   1112 
   1113             // All accounts
   1114             sURIMatcher.addURI(EmailContent.AUTHORITY, "account", ACCOUNT);
   1115             // A specific account
   1116             // insert into this URI causes a mailbox to be added to the account
   1117             sURIMatcher.addURI(EmailContent.AUTHORITY, "account/#", ACCOUNT_ID);
   1118             sURIMatcher.addURI(EmailContent.AUTHORITY, "accountCheck/#", ACCOUNT_CHECK);
   1119 
   1120             // All mailboxes
   1121             sURIMatcher.addURI(EmailContent.AUTHORITY, "mailbox", MAILBOX);
   1122             // A specific mailbox
   1123             // insert into this URI causes a message to be added to the mailbox
   1124             // ** NOTE For now, the accountKey must be set manually in the values!
   1125             sURIMatcher.addURI(EmailContent.AUTHORITY, "mailbox/*", MAILBOX_ID);
   1126             sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxNotification/#",
   1127                     MAILBOX_NOTIFICATION);
   1128             sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxMostRecentMessage/#",
   1129                     MAILBOX_MOST_RECENT_MESSAGE);
   1130             sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxCount/#", MAILBOX_MESSAGE_COUNT);
   1131 
   1132             // All messages
   1133             sURIMatcher.addURI(EmailContent.AUTHORITY, "message", MESSAGE);
   1134             // A specific message
   1135             // insert into this URI causes an attachment to be added to the message
   1136             sURIMatcher.addURI(EmailContent.AUTHORITY, "message/#", MESSAGE_ID);
   1137 
   1138             // A specific attachment
   1139             sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment", ATTACHMENT);
   1140             // A specific attachment (the header information)
   1141             sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/#", ATTACHMENT_ID);
   1142             // The attachments of a specific message (query only) (insert & delete TBD)
   1143             sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/message/#",
   1144                     ATTACHMENTS_MESSAGE_ID);
   1145             sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/cachedFile",
   1146                     ATTACHMENTS_CACHED_FILE_ACCESS);
   1147 
   1148             // All mail bodies
   1149             sURIMatcher.addURI(EmailContent.AUTHORITY, "body", BODY);
   1150             // A specific mail body
   1151             sURIMatcher.addURI(EmailContent.AUTHORITY, "body/#", BODY_ID);
   1152             // A specific HTML body part, for openFile
   1153             sURIMatcher.addURI(EmailContent.AUTHORITY, "bodyHtml/#", BODY_HTML);
   1154             // A specific text body part, for openFile
   1155             sURIMatcher.addURI(EmailContent.AUTHORITY, "bodyText/#", BODY_TEXT);
   1156 
   1157             // All hostauth records
   1158             sURIMatcher.addURI(EmailContent.AUTHORITY, "hostauth", HOSTAUTH);
   1159             // A specific hostauth
   1160             sURIMatcher.addURI(EmailContent.AUTHORITY, "hostauth/*", HOSTAUTH_ID);
   1161 
   1162             // All credential records
   1163             sURIMatcher.addURI(EmailContent.AUTHORITY, "credential", CREDENTIAL);
   1164             // A specific credential
   1165             sURIMatcher.addURI(EmailContent.AUTHORITY, "credential/*", CREDENTIAL_ID);
   1166 
   1167             /**
   1168              * THIS URI HAS SPECIAL SEMANTICS
   1169              * ITS USE IS INTENDED FOR THE UI TO MARK CHANGES THAT NEED TO BE SYNCED BACK
   1170              * TO A SERVER VIA A SYNC ADAPTER
   1171              */
   1172             sURIMatcher.addURI(EmailContent.AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID);
   1173             sURIMatcher.addURI(EmailContent.AUTHORITY, "messageBySelection", MESSAGE_SELECTION);
   1174 
   1175             sURIMatcher.addURI(EmailContent.AUTHORITY, MessageMove.PATH, MESSAGE_MOVE);
   1176             sURIMatcher.addURI(EmailContent.AUTHORITY, MessageStateChange.PATH,
   1177                     MESSAGE_STATE_CHANGE);
   1178 
   1179             /**
   1180              * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY
   1181              * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI
   1182              * BY THE UI APPLICATION
   1183              */
   1184             // All deleted messages
   1185             sURIMatcher.addURI(EmailContent.AUTHORITY, "deletedMessage", DELETED_MESSAGE);
   1186             // A specific deleted message
   1187             sURIMatcher.addURI(EmailContent.AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID);
   1188 
   1189             // All updated messages
   1190             sURIMatcher.addURI(EmailContent.AUTHORITY, "updatedMessage", UPDATED_MESSAGE);
   1191             // A specific updated message
   1192             sURIMatcher.addURI(EmailContent.AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID);
   1193 
   1194             sURIMatcher.addURI(EmailContent.AUTHORITY, "policy", POLICY);
   1195             sURIMatcher.addURI(EmailContent.AUTHORITY, "policy/#", POLICY_ID);
   1196 
   1197             // All quick responses
   1198             sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse", QUICK_RESPONSE);
   1199             // A specific quick response
   1200             sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse/#", QUICK_RESPONSE_ID);
   1201             // All quick responses associated with a particular account id
   1202             sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse/account/#",
   1203                     QUICK_RESPONSE_ACCOUNT_ID);
   1204 
   1205             sURIMatcher.addURI(EmailContent.AUTHORITY, "uifolders/#", UI_FOLDERS);
   1206             sURIMatcher.addURI(EmailContent.AUTHORITY, "uifullfolders/#", UI_FULL_FOLDERS);
   1207             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiallfolders/#", UI_ALL_FOLDERS);
   1208             sURIMatcher.addURI(EmailContent.AUTHORITY, "uisubfolders/#", UI_SUBFOLDERS);
   1209             sURIMatcher.addURI(EmailContent.AUTHORITY, "uimessages/#", UI_MESSAGES);
   1210             sURIMatcher.addURI(EmailContent.AUTHORITY, "uimessage/#", UI_MESSAGE);
   1211             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiundo", UI_UNDO);
   1212             sURIMatcher.addURI(EmailContent.AUTHORITY, QUERY_UIREFRESH + "/#", UI_FOLDER_REFRESH);
   1213             // We listen to everything trailing uifolder/ since there might be an appVersion
   1214             // as in Utils.appendVersionQueryParameter().
   1215             sURIMatcher.addURI(EmailContent.AUTHORITY, "uifolder/*", UI_FOLDER);
   1216             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiinbox/#", UI_INBOX);
   1217             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccount/#", UI_ACCOUNT);
   1218             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccts", UI_ACCTS);
   1219             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiacctsettings", UI_ACCTSETTINGS);
   1220             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiattachments/#", UI_ATTACHMENTS);
   1221             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiattachment/#", UI_ATTACHMENT);
   1222             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiattachmentbycid/#/*",
   1223                     UI_ATTACHMENT_BY_CID);
   1224             sURIMatcher.addURI(EmailContent.AUTHORITY, "uisearch/#", UI_SEARCH);
   1225             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccountdata/#", UI_ACCOUNT_DATA);
   1226             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiloadmore/#", UI_FOLDER_LOAD_MORE);
   1227             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiconversation/#", UI_CONVERSATION);
   1228             sURIMatcher.addURI(EmailContent.AUTHORITY, "uirecentfolders/#", UI_RECENT_FOLDERS);
   1229             sURIMatcher.addURI(EmailContent.AUTHORITY, "uidefaultrecentfolders/#",
   1230                     UI_DEFAULT_RECENT_FOLDERS);
   1231             sURIMatcher.addURI(EmailContent.AUTHORITY, "pickTrashFolder/#",
   1232                     ACCOUNT_PICK_TRASH_FOLDER);
   1233             sURIMatcher.addURI(EmailContent.AUTHORITY, "pickSentFolder/#",
   1234                     ACCOUNT_PICK_SENT_FOLDER);
   1235             sURIMatcher.addURI(EmailContent.AUTHORITY, "uipurgefolder/#", UI_PURGE_FOLDER);
   1236         }
   1237     }
   1238 
   1239     /**
   1240      * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must
   1241      * always be in sync (i.e. there are two database or NO databases).  This code will delete
   1242      * any "orphan" database, so that both will be created together.  Note that an "orphan" database
   1243      * will exist after either of the individual databases is deleted due to data corruption.
   1244      */
   1245     public void checkDatabases() {
   1246         synchronized (sDatabaseLock) {
   1247             // Uncache the databases
   1248             if (mDatabase != null) {
   1249                 mDatabase = null;
   1250             }
   1251             if (mBodyDatabase != null) {
   1252                 mBodyDatabase = null;
   1253             }
   1254             // Look for orphans, and delete as necessary; these must always be in sync
   1255             final File databaseFile = getContext().getDatabasePath(DATABASE_NAME);
   1256             final File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME);
   1257 
   1258             // TODO Make sure attachments are deleted
   1259             if (databaseFile.exists() && !bodyFile.exists()) {
   1260                 LogUtils.w(TAG, "Deleting orphaned EmailProvider database...");
   1261                 getContext().deleteDatabase(DATABASE_NAME);
   1262             } else if (bodyFile.exists() && !databaseFile.exists()) {
   1263                 LogUtils.w(TAG, "Deleting orphaned EmailProviderBody database...");
   1264                 getContext().deleteDatabase(BODY_DATABASE_NAME);
   1265             }
   1266         }
   1267     }
   1268 
   1269     @Override
   1270     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
   1271             String sortOrder) {
   1272         Cursor c = null;
   1273         int match;
   1274         try {
   1275             match = findMatch(uri, "query");
   1276         } catch (IllegalArgumentException e) {
   1277             String uriString = uri.toString();
   1278             // If we were passed an illegal uri, see if it ends in /-1
   1279             // if so, and if substituting 0 for -1 results in a valid uri, return an empty cursor
   1280             if (uriString != null && uriString.endsWith("/-1")) {
   1281                 uri = Uri.parse(uriString.substring(0, uriString.length() - 2) + "0");
   1282                 match = findMatch(uri, "query");
   1283                 switch (match) {
   1284                     case BODY_ID:
   1285                     case MESSAGE_ID:
   1286                     case DELETED_MESSAGE_ID:
   1287                     case UPDATED_MESSAGE_ID:
   1288                     case ATTACHMENT_ID:
   1289                     case MAILBOX_ID:
   1290                     case ACCOUNT_ID:
   1291                     case HOSTAUTH_ID:
   1292                     case CREDENTIAL_ID:
   1293                     case POLICY_ID:
   1294                         return new MatrixCursorWithCachedColumns(projection, 0);
   1295                 }
   1296             }
   1297             throw e;
   1298         }
   1299         Context context = getContext();
   1300         // See the comment at delete(), above
   1301         SQLiteDatabase db = getDatabase(context);
   1302         int table = match >> BASE_SHIFT;
   1303         String limit = uri.getQueryParameter(EmailContent.PARAMETER_LIMIT);
   1304         String id;
   1305 
   1306         String tableName = TABLE_NAMES.valueAt(table);
   1307 
   1308         try {
   1309             switch (match) {
   1310                 // First, dispatch queries from UnifiedEmail
   1311                 case UI_SEARCH:
   1312                     c = uiSearch(uri, projection);
   1313                     return c;
   1314                 case UI_ACCTS:
   1315                     final String suppressParam =
   1316                             uri.getQueryParameter(EmailContent.SUPPRESS_COMBINED_ACCOUNT_PARAM);
   1317                     final boolean suppressCombined =
   1318                             suppressParam != null && Boolean.parseBoolean(suppressParam);
   1319                     c = uiAccounts(projection, suppressCombined);
   1320                     return c;
   1321                 case UI_UNDO:
   1322                     return uiUndo(projection);
   1323                 case UI_SUBFOLDERS:
   1324                 case UI_MESSAGES:
   1325                 case UI_MESSAGE:
   1326                 case UI_FOLDER:
   1327                 case UI_INBOX:
   1328                 case UI_ACCOUNT:
   1329                 case UI_ATTACHMENT:
   1330                 case UI_ATTACHMENTS:
   1331                 case UI_ATTACHMENT_BY_CID:
   1332                 case UI_CONVERSATION:
   1333                 case UI_RECENT_FOLDERS:
   1334                 case UI_FULL_FOLDERS:
   1335                 case UI_ALL_FOLDERS:
   1336                     // For now, we don't allow selection criteria within these queries
   1337                     if (selection != null || selectionArgs != null) {
   1338                         throw new IllegalArgumentException("UI queries can't have selection/args");
   1339                     }
   1340 
   1341                     final String seenParam = uri.getQueryParameter(UIProvider.SEEN_QUERY_PARAMETER);
   1342                     final boolean unseenOnly =
   1343                             seenParam != null && Boolean.FALSE.toString().equals(seenParam);
   1344 
   1345                     c = uiQuery(match, uri, projection, unseenOnly);
   1346                     return c;
   1347                 case UI_FOLDERS:
   1348                     c = uiFolders(uri, projection);
   1349                     return c;
   1350                 case UI_FOLDER_LOAD_MORE:
   1351                     c = uiFolderLoadMore(getMailbox(uri));
   1352                     return c;
   1353                 case UI_FOLDER_REFRESH:
   1354                     c = uiFolderRefresh(getMailbox(uri), 0);
   1355                     return c;
   1356                 case MAILBOX_NOTIFICATION:
   1357                     c = notificationQuery(uri);
   1358                     return c;
   1359                 case MAILBOX_MOST_RECENT_MESSAGE:
   1360                     c = mostRecentMessageQuery(uri);
   1361                     return c;
   1362                 case MAILBOX_MESSAGE_COUNT:
   1363                     c = getMailboxMessageCount(uri);
   1364                     return c;
   1365                 case MESSAGE_MOVE:
   1366                     return db.query(MessageMove.TABLE_NAME, projection, selection, selectionArgs,
   1367                             null, null, sortOrder, limit);
   1368                 case MESSAGE_STATE_CHANGE:
   1369                     return db.query(MessageStateChange.TABLE_NAME, projection, selection,
   1370                             selectionArgs, null, null, sortOrder, limit);
   1371                 case MESSAGE:
   1372                 case UPDATED_MESSAGE:
   1373                 case DELETED_MESSAGE:
   1374                 case ATTACHMENT:
   1375                 case MAILBOX:
   1376                 case ACCOUNT:
   1377                 case HOSTAUTH:
   1378                 case CREDENTIAL:
   1379                 case POLICY:
   1380                     c = db.query(tableName, projection,
   1381                             selection, selectionArgs, null, null, sortOrder, limit);
   1382                     break;
   1383                 case QUICK_RESPONSE:
   1384                     c = uiQuickResponse(projection);
   1385                     break;
   1386                 case BODY:
   1387                 case BODY_ID: {
   1388                     final ProjectionMap map = new ProjectionMap.Builder()
   1389                             .addAll(projection)
   1390                             .build();
   1391                     if (map.containsKey(BodyColumns.HTML_CONTENT) ||
   1392                             map.containsKey(BodyColumns.TEXT_CONTENT)) {
   1393                         throw new IllegalArgumentException(
   1394                                 "Body content cannot be returned in the cursor");
   1395                     }
   1396 
   1397                     final ContentValues cv = new ContentValues(2);
   1398                     cv.put(BodyColumns.HTML_CONTENT_URI, "@" + uriWithColumn("bodyHtml",
   1399                             BodyColumns.MESSAGE_KEY));
   1400                     cv.put(BodyColumns.TEXT_CONTENT_URI, "@" + uriWithColumn("bodyText",
   1401                             BodyColumns.MESSAGE_KEY));
   1402 
   1403                     final StringBuilder sb = genSelect(map, projection, cv);
   1404                     sb.append(" FROM ").append(Body.TABLE_NAME);
   1405                     if (match == BODY_ID) {
   1406                         id = uri.getPathSegments().get(1);
   1407                         sb.append(" WHERE ").append(whereWithId(id, selection));
   1408                     } else if (!TextUtils.isEmpty(selection)) {
   1409                         sb.append(" WHERE ").append(selection);
   1410                     }
   1411                     if (!TextUtils.isEmpty(sortOrder)) {
   1412                         sb.append(" ORDER BY ").append(sortOrder);
   1413                     }
   1414                     if (!TextUtils.isEmpty(limit)) {
   1415                         sb.append(" LIMIT ").append(limit);
   1416                     }
   1417                     c = db.rawQuery(sb.toString(), selectionArgs);
   1418                     break;
   1419                 }
   1420                 case MESSAGE_ID:
   1421                 case DELETED_MESSAGE_ID:
   1422                 case UPDATED_MESSAGE_ID:
   1423                 case ATTACHMENT_ID:
   1424                 case MAILBOX_ID:
   1425                 case HOSTAUTH_ID:
   1426                 case CREDENTIAL_ID:
   1427                 case POLICY_ID:
   1428                     id = uri.getPathSegments().get(1);
   1429                     c = db.query(tableName, projection, whereWithId(id, selection),
   1430                             selectionArgs, null, null, sortOrder, limit);
   1431                     break;
   1432                 case ACCOUNT_ID:
   1433                     id = uri.getPathSegments().get(1);
   1434                     // There seems to be an issue with smart forwarding sometimes including the
   1435                     // quoted text from the wrong message. For now, we just disable it.
   1436                     final String[] alternateProjection = new String[projection.length];
   1437                     for (int i = 0; i < projection.length; i++) {
   1438                         String column = projection[i];
   1439                         if (TextUtils.equals(column, AccountColumns.FLAGS)) {
   1440                             alternateProjection[i] = AccountColumns.FLAGS + " & ~" +
   1441                                     Account.FLAGS_SUPPORTS_SMART_FORWARD + " AS " +
   1442                                     AccountColumns.FLAGS;
   1443                         } else {
   1444                             alternateProjection[i] = projection[i];
   1445                         }
   1446                     }
   1447 
   1448                     c = db.query(tableName, alternateProjection, whereWithId(id, selection),
   1449                             selectionArgs, null, null, sortOrder, limit);
   1450                     break;
   1451                 case QUICK_RESPONSE_ID:
   1452                     id = uri.getPathSegments().get(1);
   1453                     c = uiQuickResponseId(projection, id);
   1454                     break;
   1455                 case ATTACHMENTS_MESSAGE_ID:
   1456                     // All attachments for the given message
   1457                     id = uri.getPathSegments().get(2);
   1458                     c = db.query(Attachment.TABLE_NAME, projection,
   1459                             whereWith(AttachmentColumns.MESSAGE_KEY + "=" + id, selection),
   1460                             selectionArgs, null, null, sortOrder, limit);
   1461                     break;
   1462                 case QUICK_RESPONSE_ACCOUNT_ID:
   1463                     // All quick responses for the given account
   1464                     id = uri.getPathSegments().get(2);
   1465                     c = uiQuickResponseAccount(projection, id);
   1466                     break;
   1467                 case ATTACHMENTS_CACHED_FILE_ACCESS:
   1468                     if (projection == null) {
   1469                         projection =
   1470                                 new String[] {
   1471                                         AttachmentUtilities.Columns._ID,
   1472                                         AttachmentUtilities.Columns.DATA,
   1473                                 };
   1474                     }
   1475                     // Map the columns of our attachment table to the columns defined in
   1476                     // AttachmentUtils. These are a superset of OpenableColumns.
   1477                     // This mirrors similar code in AttachmentProvider.
   1478                     c = db.query(Attachment.TABLE_NAME,
   1479                             CACHED_FILE_QUERY_PROJECTION, AttachmentColumns.CACHED_FILE + "=?",
   1480                             new String[]{uri.toString()}, null, null, null, null);
   1481                     try {
   1482                         if (c.getCount() > 1) {
   1483                             LogUtils.e(TAG, "multiple results querying CACHED_FILE_ACCESS %s", uri);
   1484                         }
   1485                         if (c != null && c.moveToFirst()) {
   1486                             MatrixCursor ret = new MatrixCursorWithCachedColumns(projection);
   1487                             Object[] values = new Object[projection.length];
   1488                             for (int i = 0, count = projection.length; i < count; i++) {
   1489                                 String column = projection[i];
   1490                                 if (AttachmentUtilities.Columns._ID.equals(column)) {
   1491                                     values[i] = c.getLong(
   1492                                             c.getColumnIndexOrThrow(AttachmentColumns._ID));
   1493                                 }
   1494                                 else if (AttachmentUtilities.Columns.DATA.equals(column)) {
   1495                                     values[i] = c.getString(
   1496                                             c.getColumnIndexOrThrow(AttachmentColumns.CONTENT_URI));
   1497                                 }
   1498                                 else if (AttachmentUtilities.Columns.DISPLAY_NAME.equals(column)) {
   1499                                     values[i] = c.getString(
   1500                                             c.getColumnIndexOrThrow(AttachmentColumns.FILENAME));
   1501                                 }
   1502                                 else if (AttachmentUtilities.Columns.SIZE.equals(column)) {
   1503                                     values[i] = c.getInt(
   1504                                             c.getColumnIndexOrThrow(AttachmentColumns.SIZE));
   1505                                 } else {
   1506                                     LogUtils.e(TAG,
   1507                                             "unexpected column %s requested for CACHED_FILE",
   1508                                             column);
   1509                                 }
   1510                             }
   1511                             ret.addRow(values);
   1512                             return ret;
   1513                         }
   1514                     } finally {
   1515                         if (c !=  null) {
   1516                             c.close();
   1517                         }
   1518                     }
   1519                     return null;
   1520                 default:
   1521                     throw new IllegalArgumentException("Unknown URI " + uri);
   1522             }
   1523         } catch (SQLiteException e) {
   1524             checkDatabases();
   1525             throw e;
   1526         } catch (RuntimeException e) {
   1527             checkDatabases();
   1528             e.printStackTrace();
   1529             throw e;
   1530         } finally {
   1531             if (c == null) {
   1532                 // This should never happen, but let's be sure to log it...
   1533                 // TODO: There are actually cases where c == null is expected, for example
   1534                 // UI_FOLDER_LOAD_MORE.
   1535                 // Demoting this to a warning for now until we figure out what to do with it.
   1536                 LogUtils.w(TAG, "Query returning null for uri: %s selection: %s", uri, selection);
   1537             }
   1538         }
   1539 
   1540         if ((c != null) && !isTemporary()) {
   1541             c.setNotificationUri(getContext().getContentResolver(), uri);
   1542         }
   1543         return c;
   1544     }
   1545 
   1546     private static String whereWithId(String id, String selection) {
   1547         StringBuilder sb = new StringBuilder(256);
   1548         sb.append("_id=");
   1549         sb.append(id);
   1550         if (selection != null) {
   1551             sb.append(" AND (");
   1552             sb.append(selection);
   1553             sb.append(')');
   1554         }
   1555         return sb.toString();
   1556     }
   1557 
   1558     /**
   1559      * Combine a locally-generated selection with a user-provided selection
   1560      *
   1561      * This introduces risk that the local selection might insert incorrect chars
   1562      * into the SQL, so use caution.
   1563      *
   1564      * @param where locally-generated selection, must not be null
   1565      * @param selection user-provided selection, may be null
   1566      * @return a single selection string
   1567      */
   1568     private static String whereWith(String where, String selection) {
   1569         if (selection == null) {
   1570             return where;
   1571         }
   1572         return where + " AND (" + selection + ")";
   1573     }
   1574 
   1575     /**
   1576      * Restore a HostAuth from a database, given its unique id
   1577      * @param db the database
   1578      * @param id the unique id (_id) of the row
   1579      * @return a fully populated HostAuth or null if the row does not exist
   1580      */
   1581     private static HostAuth restoreHostAuth(SQLiteDatabase db, long id) {
   1582         Cursor c = db.query(HostAuth.TABLE_NAME, HostAuth.CONTENT_PROJECTION,
   1583                 HostAuthColumns._ID + "=?", new String[] {Long.toString(id)}, null, null, null);
   1584         try {
   1585             if (c.moveToFirst()) {
   1586                 HostAuth hostAuth = new HostAuth();
   1587                 hostAuth.restore(c);
   1588                 return hostAuth;
   1589             }
   1590             return null;
   1591         } finally {
   1592             c.close();
   1593         }
   1594     }
   1595 
   1596     /**
   1597      * Copy the Account and HostAuth tables from one database to another
   1598      * @param fromDatabase the source database
   1599      * @param toDatabase the destination database
   1600      * @return the number of accounts copied, or -1 if an error occurred
   1601      */
   1602     private static int copyAccountTables(SQLiteDatabase fromDatabase, SQLiteDatabase toDatabase) {
   1603         if (fromDatabase == null || toDatabase == null) return -1;
   1604 
   1605         // Lock both databases; for the "from" database, we don't want anyone changing it from
   1606         // under us; for the "to" database, we want to make the operation atomic
   1607         int copyCount = 0;
   1608         fromDatabase.beginTransaction();
   1609         try {
   1610             toDatabase.beginTransaction();
   1611             try {
   1612                 // Delete anything hanging around here
   1613                 toDatabase.delete(Account.TABLE_NAME, null, null);
   1614                 toDatabase.delete(HostAuth.TABLE_NAME, null, null);
   1615 
   1616                 // Get our account cursor
   1617                 Cursor c = fromDatabase.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION,
   1618                         null, null, null, null, null);
   1619                 if (c == null) return 0;
   1620                 LogUtils.d(TAG, "fromDatabase accounts: " + c.getCount());
   1621                 try {
   1622                     // Loop through accounts, copying them and associated host auth's
   1623                     while (c.moveToNext()) {
   1624                         Account account = new Account();
   1625                         account.restore(c);
   1626 
   1627                         // Clear security sync key and sync key, as these were specific to the
   1628                         // state of the account, and we've reset that...
   1629                         // Clear policy key so that we can re-establish policies from the server
   1630                         // TODO This is pretty EAS specific, but there's a lot of that around
   1631                         account.mSecuritySyncKey = null;
   1632                         account.mSyncKey = null;
   1633                         account.mPolicyKey = 0;
   1634 
   1635                         // Copy host auth's and update foreign keys
   1636                         HostAuth hostAuth = restoreHostAuth(fromDatabase,
   1637                                 account.mHostAuthKeyRecv);
   1638 
   1639                         // The account might have gone away, though very unlikely
   1640                         if (hostAuth == null) continue;
   1641                         account.mHostAuthKeyRecv = toDatabase.insert(HostAuth.TABLE_NAME, null,
   1642                                 hostAuth.toContentValues());
   1643 
   1644                         // EAS accounts have no send HostAuth
   1645                         if (account.mHostAuthKeySend > 0) {
   1646                             hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeySend);
   1647                             // Belt and suspenders; I can't imagine that this is possible,
   1648                             // since we checked the validity of the account above, and the
   1649                             // database is now locked
   1650                             if (hostAuth == null) continue;
   1651                             account.mHostAuthKeySend = toDatabase.insert(
   1652                                     HostAuth.TABLE_NAME, null, hostAuth.toContentValues());
   1653                         }
   1654 
   1655                         // Now, create the account in the "to" database
   1656                         toDatabase.insert(Account.TABLE_NAME, null, account.toContentValues());
   1657                         copyCount++;
   1658                     }
   1659                 } finally {
   1660                     c.close();
   1661                 }
   1662 
   1663                 // Say it's ok to commit
   1664                 toDatabase.setTransactionSuccessful();
   1665             } finally {
   1666                 toDatabase.endTransaction();
   1667             }
   1668         } catch (SQLiteException ex) {
   1669             LogUtils.w(TAG, "Exception while copying account tables", ex);
   1670             copyCount = -1;
   1671         } finally {
   1672             fromDatabase.endTransaction();
   1673         }
   1674         return copyCount;
   1675     }
   1676 
   1677     /**
   1678      * Backup account data, returning the number of accounts backed up
   1679      */
   1680     private static int backupAccounts(final Context context, final SQLiteDatabase db) {
   1681         final AccountManager am = AccountManager.get(context);
   1682         final Cursor accountCursor = db.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION,
   1683                 null, null, null, null, null);
   1684         int updatedCount = 0;
   1685         try {
   1686             while (accountCursor.moveToNext()) {
   1687                 final Account account = new Account();
   1688                 account.restore(accountCursor);
   1689                 EmailServiceInfo serviceInfo =
   1690                         EmailServiceUtils.getServiceInfo(context, account.getProtocol(context));
   1691                 if (serviceInfo == null) {
   1692                     LogUtils.d(LogUtils.TAG, "Could not find service info for account");
   1693                     continue;
   1694                 }
   1695                 final String jsonString = account.toJsonString(context);
   1696                 final android.accounts.Account amAccount =
   1697                         account.getAccountManagerAccount(serviceInfo.accountType);
   1698                 am.setUserData(amAccount, ACCOUNT_MANAGER_JSON_TAG, jsonString);
   1699                 updatedCount++;
   1700             }
   1701         } finally {
   1702             accountCursor.close();
   1703         }
   1704         return updatedCount;
   1705     }
   1706 
   1707     /**
   1708      * Restore account data, returning the number of accounts restored
   1709      */
   1710     private static int restoreAccounts(final Context context) {
   1711         final Collection<EmailServiceInfo> infos = EmailServiceUtils.getServiceInfoList(context);
   1712         // Find all possible account types
   1713         final Set<String> accountTypes = new HashSet<String>(3);
   1714         for (final EmailServiceInfo info : infos) {
   1715             if (!TextUtils.isEmpty(info.accountType)) {
   1716                 // accountType will be empty for the gmail stub entry
   1717                 accountTypes.add(info.accountType);
   1718             }
   1719         }
   1720         // Find all accounts we own
   1721         final List<android.accounts.Account> amAccounts = new ArrayList<android.accounts.Account>();
   1722         final AccountManager am = AccountManager.get(context);
   1723         for (final String accountType : accountTypes) {
   1724             amAccounts.addAll(Arrays.asList(am.getAccountsByType(accountType)));
   1725         }
   1726         // Try to restore them from saved JSON
   1727         int restoredCount = 0;
   1728         for (final android.accounts.Account amAccount : amAccounts) {
   1729             final String jsonString = am.getUserData(amAccount, ACCOUNT_MANAGER_JSON_TAG);
   1730             if (TextUtils.isEmpty(jsonString)) {
   1731                 continue;
   1732             }
   1733             final Account account = Account.fromJsonString(jsonString);
   1734             if (account != null) {
   1735                 AccountSettingsUtils.commitSettings(context, account);
   1736                 final Bundle extras = new Bundle(3);
   1737                 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
   1738                 extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true);
   1739                 extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
   1740                 ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras);
   1741                 restoredCount++;
   1742             }
   1743         }
   1744         return restoredCount;
   1745     }
   1746 
   1747     private static final String MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX = "insert into %s ("
   1748             + MessageChangeLogTable.MESSAGE_KEY + "," + MessageChangeLogTable.SERVER_ID + ","
   1749             + MessageChangeLogTable.ACCOUNT_KEY + "," + MessageChangeLogTable.STATUS + ",";
   1750 
   1751     private static final String MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX = ") values (%s, "
   1752             + "(select " + MessageColumns.SERVER_ID + " from " +
   1753                     Message.TABLE_NAME + " where _id=%s),"
   1754             + "(select " + MessageColumns.ACCOUNT_KEY + " from " +
   1755                     Message.TABLE_NAME + " where _id=%s),"
   1756             + MessageMove.STATUS_NONE_STRING + ",";
   1757 
   1758     /**
   1759      * Formatting string to generate the SQL statement for inserting into MessageMove.
   1760      * The formatting parameters are:
   1761      * table name, message id x 4, destination folder id, message id, destination folder id.
   1762      * Duplications are needed for sub-selects.
   1763      */
   1764     private static final String MESSAGE_MOVE_INSERT = MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX
   1765             + MessageMove.SRC_FOLDER_KEY + "," + MessageMove.DST_FOLDER_KEY + ","
   1766             + MessageMove.SRC_FOLDER_SERVER_ID + "," + MessageMove.DST_FOLDER_SERVER_ID
   1767             + MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX
   1768             + "(select " + MessageColumns.MAILBOX_KEY +
   1769                     " from " + Message.TABLE_NAME + " where _id=%s)," + "%d,"
   1770             + "(select " + Mailbox.SERVER_ID + " from " + Mailbox.TABLE_NAME + " where _id=(select "
   1771             + MessageColumns.MAILBOX_KEY + " from " + Message.TABLE_NAME + " where _id=%s)),"
   1772             + "(select " + Mailbox.SERVER_ID + " from " + Mailbox.TABLE_NAME + " where _id=%d))";
   1773 
   1774     /**
   1775      * Insert a row into the MessageMove table when that message is moved.
   1776      * @param db The {@link SQLiteDatabase}.
   1777      * @param messageId The id of the message being moved.
   1778      * @param dstFolderKey The folder to which the message is being moved.
   1779      */
   1780     private void addToMessageMove(final SQLiteDatabase db, final String messageId,
   1781             final long dstFolderKey) {
   1782         db.execSQL(String.format(Locale.US, MESSAGE_MOVE_INSERT, MessageMove.TABLE_NAME,
   1783                 messageId, messageId, messageId, messageId, dstFolderKey, messageId, dstFolderKey));
   1784     }
   1785 
   1786     /**
   1787      * Formatting string to generate the SQL statement for inserting into MessageStateChange.
   1788      * The formatting parameters are:
   1789      * table name, message id x 4, new flag read, message id, new flag favorite.
   1790      * Duplications are needed for sub-selects.
   1791      */
   1792     private static final String MESSAGE_STATE_CHANGE_INSERT = MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX
   1793             + MessageStateChange.OLD_FLAG_READ + "," + MessageStateChange.NEW_FLAG_READ + ","
   1794             + MessageStateChange.OLD_FLAG_FAVORITE + "," + MessageStateChange.NEW_FLAG_FAVORITE
   1795             + MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX
   1796             + "(select " + MessageColumns.FLAG_READ +
   1797             " from " + Message.TABLE_NAME + " where _id=%s)," + "%d,"
   1798             + "(select " + MessageColumns.FLAG_FAVORITE +
   1799             " from " + Message.TABLE_NAME + " where _id=%s)," + "%d)";
   1800 
   1801     private void addToMessageStateChange(final SQLiteDatabase db, final String messageId,
   1802             final int newFlagRead, final int newFlagFavorite) {
   1803         db.execSQL(String.format(Locale.US, MESSAGE_STATE_CHANGE_INSERT,
   1804                 MessageStateChange.TABLE_NAME, messageId, messageId, messageId, messageId,
   1805                 newFlagRead, messageId, newFlagFavorite));
   1806     }
   1807 
   1808     // select count(*) from (select count(*) as dupes from Mailbox where accountKey=?
   1809     // group by serverId) where dupes > 1;
   1810     private static final String ACCOUNT_INTEGRITY_SQL =
   1811             "select count(*) from (select count(*) as dupes from " + Mailbox.TABLE_NAME +
   1812             " where accountKey=? group by " + MailboxColumns.SERVER_ID + ") where dupes > 1";
   1813 
   1814 
   1815     // Query to get the protocol for a message. Temporary to switch between new and old upsync
   1816     // behavior; should go away when IMAP gets converted.
   1817     private static final String GET_MESSAGE_DETAILS = "SELECT"
   1818             + " h." + HostAuthColumns.PROTOCOL + ","
   1819             + " m." + MessageColumns.MAILBOX_KEY + ","
   1820             + " a." + AccountColumns._ID
   1821             + " FROM " + Message.TABLE_NAME + " AS m"
   1822             + " INNER JOIN " + Account.TABLE_NAME + " AS a"
   1823             + " ON m." + MessageColumns.ACCOUNT_KEY + "=a." + AccountColumns._ID
   1824             + " INNER JOIN " + HostAuth.TABLE_NAME + " AS h"
   1825             + " ON a." + AccountColumns.HOST_AUTH_KEY_RECV + "=h." + HostAuthColumns._ID
   1826             + " WHERE m." + MessageColumns._ID + "=?";
   1827     private static final int INDEX_PROTOCOL = 0;
   1828     private static final int INDEX_MAILBOX_KEY = 1;
   1829     private static final int INDEX_ACCOUNT_KEY = 2;
   1830 
   1831     /**
   1832      * Query to get the protocol and email address for an account. Note that this uses
   1833      * {@link #INDEX_PROTOCOL} and {@link #INDEX_EMAIL_ADDRESS} for its columns.
   1834      */
   1835     private static final String GET_ACCOUNT_DETAILS = "SELECT"
   1836             + " h." + HostAuthColumns.PROTOCOL + ","
   1837             + " a." + AccountColumns.EMAIL_ADDRESS + ","
   1838             + " a." + AccountColumns.SYNC_KEY
   1839             + " FROM " + Account.TABLE_NAME + " AS a"
   1840             + " INNER JOIN " + HostAuth.TABLE_NAME + " AS h"
   1841             + " ON a." + AccountColumns.HOST_AUTH_KEY_RECV + "=h." + HostAuthColumns._ID
   1842             + " WHERE a." + AccountColumns._ID + "=?";
   1843     private static final int INDEX_EMAIL_ADDRESS = 1;
   1844     private static final int INDEX_SYNC_KEY = 2;
   1845 
   1846     /**
   1847      * Restart push if we need it (currently only for Exchange accounts).
   1848      * @param context A {@link Context}.
   1849      * @param db The {@link SQLiteDatabase}.
   1850      * @param id The id of the thing we're looking for.
   1851      * @return Whether or not we sent a request to restart the push.
   1852      */
   1853     private static boolean restartPush(final Context context, final SQLiteDatabase db,
   1854             final String id) {
   1855         final Cursor c = db.rawQuery(GET_ACCOUNT_DETAILS, new String[] {id});
   1856         if (c != null) {
   1857             try {
   1858                 if (c.moveToFirst()) {
   1859                     final String protocol = c.getString(INDEX_PROTOCOL);
   1860                     // Only restart push for EAS accounts that have completed initial sync.
   1861                     if (context.getString(R.string.protocol_eas).equals(protocol) &&
   1862                             !EmailContent.isInitialSyncKey(c.getString(INDEX_SYNC_KEY))) {
   1863                         final String emailAddress = c.getString(INDEX_EMAIL_ADDRESS);
   1864                         final android.accounts.Account account =
   1865                                 getAccountManagerAccount(context, emailAddress, protocol);
   1866                         if (account != null) {
   1867                             restartPush(account);
   1868                             return true;
   1869                         }
   1870                     }
   1871                 }
   1872             } finally {
   1873                 c.close();
   1874             }
   1875         }
   1876         return false;
   1877     }
   1878 
   1879     /**
   1880      * Restart push if a mailbox's settings change in a way that requires it.
   1881      * @param context A {@link Context}.
   1882      * @param db The {@link SQLiteDatabase}.
   1883      * @param values The {@link ContentValues} that were updated for the mailbox.
   1884      * @param accountId The id of the account for this mailbox.
   1885      * @return Whether or not the push was restarted.
   1886      */
   1887     private static boolean restartPushForMailbox(final Context context, final SQLiteDatabase db,
   1888             final ContentValues values, final String accountId) {
   1889         if (values.containsKey(MailboxColumns.SYNC_LOOKBACK) ||
   1890                 values.containsKey(MailboxColumns.SYNC_INTERVAL)) {
   1891             return restartPush(context, db, accountId);
   1892         }
   1893         return false;
   1894     }
   1895 
   1896     /**
   1897      * Restart push if an account's settings change in a way that requires it.
   1898      * @param context A {@link Context}.
   1899      * @param db The {@link SQLiteDatabase}.
   1900      * @param values The {@link ContentValues} that were updated for the account.
   1901      * @param accountId The id of the account.
   1902      * @return Whether or not the push was restarted.
   1903      */
   1904     private static boolean restartPushForAccount(final Context context, final SQLiteDatabase db,
   1905             final ContentValues values, final String accountId) {
   1906         if (values.containsKey(AccountColumns.SYNC_LOOKBACK) ||
   1907                 values.containsKey(AccountColumns.SYNC_INTERVAL)) {
   1908             return restartPush(context, db, accountId);
   1909         }
   1910         return false;
   1911     }
   1912 
   1913     @Override
   1914     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
   1915         LogUtils.d(TAG, "Update: " + uri);
   1916         // Handle this special case the fastest possible way
   1917         if (INTEGRITY_CHECK_URI.equals(uri)) {
   1918             checkDatabases();
   1919             return 0;
   1920         } else if (ACCOUNT_BACKUP_URI.equals(uri)) {
   1921             return backupAccounts(getContext(), getDatabase(getContext()));
   1922         }
   1923 
   1924         // Notify all existing cursors, except for ACCOUNT_RESET_NEW_COUNT(_ID)
   1925         Uri notificationUri = EmailContent.CONTENT_URI;
   1926 
   1927         final int match = findMatch(uri, "update");
   1928         final Context context = getContext();
   1929         // See the comment at delete(), above
   1930         final SQLiteDatabase db = getDatabase(context);
   1931         final int table = match >> BASE_SHIFT;
   1932         int result;
   1933 
   1934         // We do NOT allow setting of unreadCount/messageCount via the provider
   1935         // These columns are maintained via triggers
   1936         if (match == MAILBOX_ID || match == MAILBOX) {
   1937             values.remove(MailboxColumns.UNREAD_COUNT);
   1938             values.remove(MailboxColumns.MESSAGE_COUNT);
   1939         }
   1940 
   1941         final String tableName = TABLE_NAMES.valueAt(table);
   1942         String id = "0";
   1943 
   1944         try {
   1945             switch (match) {
   1946                 case ACCOUNT_PICK_TRASH_FOLDER:
   1947                     return pickTrashFolder(uri);
   1948                 case ACCOUNT_PICK_SENT_FOLDER:
   1949                     return pickSentFolder(uri);
   1950                 case UI_ACCTSETTINGS:
   1951                     return uiUpdateSettings(context, values);
   1952                 case UI_FOLDER:
   1953                     return uiUpdateFolder(context, uri, values);
   1954                 case UI_RECENT_FOLDERS:
   1955                     return uiUpdateRecentFolders(uri, values);
   1956                 case UI_DEFAULT_RECENT_FOLDERS:
   1957                     return uiPopulateRecentFolders(uri);
   1958                 case UI_ATTACHMENT:
   1959                     return uiUpdateAttachment(uri, values);
   1960                 case UI_MESSAGE:
   1961                     return uiUpdateMessage(uri, values);
   1962                 case ACCOUNT_CHECK:
   1963                     id = uri.getLastPathSegment();
   1964                     // With any error, return 1 (a failure)
   1965                     int res = 1;
   1966                     Cursor ic = null;
   1967                     try {
   1968                         ic = db.rawQuery(ACCOUNT_INTEGRITY_SQL, new String[] {id});
   1969                         if (ic.moveToFirst()) {
   1970                             res = ic.getInt(0);
   1971                         }
   1972                     } finally {
   1973                         if (ic != null) {
   1974                             ic.close();
   1975                         }
   1976                     }
   1977                     // Count of duplicated mailboxes
   1978                     return res;
   1979                 case MESSAGE_SELECTION:
   1980                     Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection,
   1981                             selectionArgs, null, null, null);
   1982                     try {
   1983                         if (findCursor.moveToFirst()) {
   1984                             return update(ContentUris.withAppendedId(
   1985                                     Message.CONTENT_URI,
   1986                                     findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)),
   1987                                     values, null, null);
   1988                         } else {
   1989                             return 0;
   1990                         }
   1991                     } finally {
   1992                         findCursor.close();
   1993                     }
   1994                 case SYNCED_MESSAGE_ID:
   1995                 case UPDATED_MESSAGE_ID:
   1996                 case MESSAGE_ID:
   1997                 case ATTACHMENT_ID:
   1998                 case MAILBOX_ID:
   1999                 case ACCOUNT_ID:
   2000                 case HOSTAUTH_ID:
   2001                 case CREDENTIAL_ID:
   2002                 case QUICK_RESPONSE_ID:
   2003                 case POLICY_ID:
   2004                     id = uri.getPathSegments().get(1);
   2005                     if (match == SYNCED_MESSAGE_ID) {
   2006                         // TODO: Migrate IMAP to use MessageMove/MessageStateChange as well.
   2007                         boolean isEas = false;
   2008                         long mailboxId = -1;
   2009                         long accountId = -1;
   2010                         final Cursor c = db.rawQuery(GET_MESSAGE_DETAILS, new String[] {id});
   2011                         if (c != null) {
   2012                             try {
   2013                                 if (c.moveToFirst()) {
   2014                                     final String protocol = c.getString(INDEX_PROTOCOL);
   2015                                     isEas = context.getString(R.string.protocol_eas)
   2016                                             .equals(protocol);
   2017                                     mailboxId = c.getLong(INDEX_MAILBOX_KEY);
   2018                                     accountId = c.getLong(INDEX_ACCOUNT_KEY);
   2019                                 }
   2020                             } finally {
   2021                                 c.close();
   2022                             }
   2023                         }
   2024 
   2025                         if (isEas) {
   2026                             // EAS uses the new upsync classes.
   2027                             Long dstFolderId = values.getAsLong(MessageColumns.MAILBOX_KEY);
   2028                             if (dstFolderId != null) {
   2029                                 addToMessageMove(db, id, dstFolderId);
   2030                             }
   2031                             Integer flagRead = values.getAsInteger(MessageColumns.FLAG_READ);
   2032                             Integer flagFavorite = values.getAsInteger(MessageColumns.FLAG_FAVORITE);
   2033                             int flagReadValue = (flagRead != null) ?
   2034                                     flagRead : MessageStateChange.VALUE_UNCHANGED;
   2035                             int flagFavoriteValue = (flagFavorite != null) ?
   2036                                     flagFavorite : MessageStateChange.VALUE_UNCHANGED;
   2037                             if (flagRead != null || flagFavorite != null) {
   2038                                 addToMessageStateChange(db, id, flagReadValue, flagFavoriteValue);
   2039                             }
   2040 
   2041                             // Request a sync for the messages mailbox so the update will upsync.
   2042                             // This is normally done with ContentResolver.notifyUpdate() but doesn't
   2043                             // work for Exchange because the Sync Adapter is declared as
   2044                             // android:supportsUploading="false". Changing it to true is not trivial
   2045                             // because that would require us to protect all calls to notifyUpdate()
   2046                             // with syncToServer=false except in cases where we actually want to
   2047                             // upsync.
   2048                             // TODO: Look into making Exchange Sync Adapter supportsUploading=true
   2049                             // Since we can't use the Sync Manager "delayed-sync" feature which
   2050                             // applies only to UPLOAD syncs, we need to do this ourselves. The
   2051                             // purpose of this is not to spam syncs when making frequent
   2052                             // modifications.
   2053                             final Handler handler = getDelayedSyncHandler();
   2054                             final android.accounts.Account amAccount =
   2055                                     getAccountManagerAccount(accountId);
   2056                             if (amAccount != null) {
   2057                                 final SyncRequestMessage request = new SyncRequestMessage(
   2058                                         uri.getAuthority(), amAccount, mailboxId);
   2059                                 synchronized (mDelayedSyncRequests) {
   2060                                     if (!mDelayedSyncRequests.contains(request)) {
   2061                                         mDelayedSyncRequests.add(request);
   2062                                         final android.os.Message message =
   2063                                                 handler.obtainMessage(0, request);
   2064                                         handler.sendMessageDelayed(message, SYNC_DELAY_MILLIS);
   2065                                     }
   2066                                 }
   2067                             } else {
   2068                                 LogUtils.d(TAG,
   2069                                         "Attempted to start delayed sync for invalid account %d",
   2070                                         accountId);
   2071                             }
   2072                         } else {
   2073                             // Old way of doing upsync.
   2074                             // For synced messages, first copy the old message to the updated table
   2075                             // Note the insert or ignore semantics, guaranteeing that only the first
   2076                             // update will be reflected in the updated message table; therefore this
   2077                             // row will always have the "original" data
   2078                             db.execSQL(UPDATED_MESSAGE_INSERT + id);
   2079                         }
   2080                     } else if (match == MESSAGE_ID) {
   2081                         db.execSQL(UPDATED_MESSAGE_DELETE + id);
   2082                     }
   2083                     result = db.update(tableName, values, whereWithId(id, selection),
   2084                             selectionArgs);
   2085                     if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) {
   2086                         handleMessageUpdateNotifications(uri, id, values);
   2087                     } else if (match == ATTACHMENT_ID) {
   2088                         long attId = Integer.parseInt(id);
   2089                         if (values.containsKey(AttachmentColumns.FLAGS)) {
   2090                             int flags = values.getAsInteger(AttachmentColumns.FLAGS);
   2091                             mAttachmentService.attachmentChanged(context, attId, flags);
   2092                         }
   2093                         // Notify UI if necessary; there are only two columns we can change that
   2094                         // would be worth a notification
   2095                         if (values.containsKey(AttachmentColumns.UI_STATE) ||
   2096                                 values.containsKey(AttachmentColumns.UI_DOWNLOADED_SIZE)) {
   2097                             // Notify on individual attachment
   2098                             notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id);
   2099                             Attachment att = Attachment.restoreAttachmentWithId(context, attId);
   2100                             if (att != null) {
   2101                                 // And on owning Message
   2102                                 notifyUI(UIPROVIDER_ATTACHMENTS_NOTIFIER, att.mMessageKey);
   2103                             }
   2104                         }
   2105                     } else if (match == MAILBOX_ID) {
   2106                         final long accountId = Mailbox.getAccountIdForMailbox(context, id);
   2107                         notifyUIFolder(id, accountId);
   2108                         restartPushForMailbox(context, db, values, Long.toString(accountId));
   2109                     } else if (match == ACCOUNT_ID) {
   2110                         updateAccountSyncInterval(Long.parseLong(id), values);
   2111                         // Notify individual account and "all accounts"
   2112                         notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id);
   2113                         notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null);
   2114                         restartPushForAccount(context, db, values, id);
   2115                     }
   2116                     break;
   2117                 case BODY_ID: {
   2118                     final ContentValues updateValues = new ContentValues(values);
   2119                     updateValues.remove(BodyColumns.HTML_CONTENT);
   2120                     updateValues.remove(BodyColumns.TEXT_CONTENT);
   2121 
   2122                     result = db.update(tableName, updateValues, whereWithId(id, selection),
   2123                             selectionArgs);
   2124 
   2125                     if (values.containsKey(BodyColumns.HTML_CONTENT) ||
   2126                             values.containsKey(BodyColumns.TEXT_CONTENT)) {
   2127                         final long messageId;
   2128                         if (values.containsKey(BodyColumns.MESSAGE_KEY)) {
   2129                             messageId = values.getAsLong(BodyColumns.MESSAGE_KEY);
   2130                         } else {
   2131                             final long bodyId = Long.parseLong(id);
   2132                             final SQLiteStatement sql = db.compileStatement(
   2133                                     "select " + BodyColumns.MESSAGE_KEY +
   2134                                             " from " + Body.TABLE_NAME +
   2135                                             " where " + BodyColumns._ID + "=" + Long
   2136                                             .toString(bodyId)
   2137                             );
   2138                             messageId = sql.simpleQueryForLong();
   2139                         }
   2140                         writeBodyFiles(context, messageId, values);
   2141                     }
   2142                     break;
   2143                 }
   2144                 case BODY: {
   2145                     final ContentValues updateValues = new ContentValues(values);
   2146                     updateValues.remove(BodyColumns.HTML_CONTENT);
   2147                     updateValues.remove(BodyColumns.TEXT_CONTENT);
   2148 
   2149                     result = db.update(tableName, updateValues, selection, selectionArgs);
   2150 
   2151                     if (result == 0 && selection.equals(Body.SELECTION_BY_MESSAGE_KEY)) {
   2152                         // TODO: This is a hack. Notably, the selection equality test above
   2153                         // is hokey at best.
   2154                         LogUtils.i(TAG, "Body Update to non-existent row, morphing to insert");
   2155                         final ContentValues insertValues = new ContentValues(values);
   2156                         insertValues.put(BodyColumns.MESSAGE_KEY, selectionArgs[0]);
   2157                         insert(Body.CONTENT_URI, insertValues);
   2158                     } else {
   2159                         // possibly need to write new body values
   2160                         if (values.containsKey(BodyColumns.HTML_CONTENT) ||
   2161                                 values.containsKey(BodyColumns.TEXT_CONTENT)) {
   2162                             final long messageIds[];
   2163                             if (values.containsKey(BodyColumns.MESSAGE_KEY)) {
   2164                                 messageIds = new long[] {values.getAsLong(BodyColumns.MESSAGE_KEY)};
   2165                             } else if (values.containsKey(BodyColumns._ID)) {
   2166                                 final long bodyId = values.getAsLong(BodyColumns._ID);
   2167                                 final SQLiteStatement sql = db.compileStatement(
   2168                                         "select " + BodyColumns.MESSAGE_KEY +
   2169                                                 " from " + Body.TABLE_NAME +
   2170                                                 " where " + BodyColumns._ID + "=" + Long
   2171                                                 .toString(bodyId)
   2172                                 );
   2173                                 messageIds = new long[] {sql.simpleQueryForLong()};
   2174                             } else {
   2175                                 final String proj[] = {BodyColumns.MESSAGE_KEY};
   2176                                 final Cursor c = db.query(Body.TABLE_NAME, proj,
   2177                                         selection, selectionArgs,
   2178                                         null, null, null);
   2179                                 try {
   2180                                     final int count = c.getCount();
   2181                                     if (count == 0) {
   2182                                         throw new IllegalStateException("Can't find body record");
   2183                                     }
   2184                                     messageIds = new long[count];
   2185                                     int i = 0;
   2186                                     while (c.moveToNext()) {
   2187                                         messageIds[i++] = c.getLong(0);
   2188                                     }
   2189                                 } finally {
   2190                                     c.close();
   2191                                 }
   2192                             }
   2193                             // This is probably overkill
   2194                             for (int i = 0; i < messageIds.length; i++) {
   2195                                 final long messageId = messageIds[i];
   2196                                 writeBodyFiles(context, messageId, values);
   2197                             }
   2198                         }
   2199                     }
   2200                     break;
   2201                 }
   2202                 case MESSAGE:
   2203                     decodeEmailAddresses(values);
   2204                 case UPDATED_MESSAGE:
   2205                 case ATTACHMENT:
   2206                 case MAILBOX:
   2207                 case ACCOUNT:
   2208                 case HOSTAUTH:
   2209                 case CREDENTIAL:
   2210                 case POLICY:
   2211                     if (match == ATTACHMENT) {
   2212                         if (values.containsKey(AttachmentColumns.LOCATION) &&
   2213                                 TextUtils.isEmpty(values.getAsString(AttachmentColumns.LOCATION))) {
   2214                             LogUtils.w(TAG, new Throwable(), "attachment with blank location");
   2215                         }
   2216                     }
   2217                     result = db.update(tableName, values, selection, selectionArgs);
   2218                     break;
   2219                 case MESSAGE_MOVE:
   2220                     result = db.update(MessageMove.TABLE_NAME, values, selection, selectionArgs);
   2221                     break;
   2222                 case MESSAGE_STATE_CHANGE:
   2223                     result = db.update(MessageStateChange.TABLE_NAME, values, selection,
   2224                             selectionArgs);
   2225                     break;
   2226                 default:
   2227                     throw new IllegalArgumentException("Unknown URI " + uri);
   2228             }
   2229         } catch (SQLiteException e) {
   2230             checkDatabases();
   2231             throw e;
   2232         }
   2233 
   2234         // Notify all notifier cursors if some records where changed in the database
   2235         if (result > 0) {
   2236             sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id);
   2237             notifyUI(notificationUri, null);
   2238         }
   2239         return result;
   2240     }
   2241 
   2242     private void updateSyncStatus(final Bundle extras) {
   2243         final long id = extras.getLong(EmailServiceStatus.SYNC_STATUS_ID);
   2244         final int statusCode = extras.getInt(EmailServiceStatus.SYNC_STATUS_CODE);
   2245         final Uri uri = ContentUris.withAppendedId(FOLDER_STATUS_URI, id);
   2246         notifyUI(uri, null);
   2247         final boolean inProgress = statusCode == EmailServiceStatus.IN_PROGRESS;
   2248         if (inProgress) {
   2249             RefreshStatusMonitor.getInstance(getContext()).setSyncStarted(id);
   2250         } else {
   2251             final int result = extras.getInt(EmailServiceStatus.SYNC_RESULT);
   2252             final ContentValues values = new ContentValues();
   2253             values.put(Mailbox.UI_LAST_SYNC_RESULT, result);
   2254             mDatabase.update(
   2255                     Mailbox.TABLE_NAME,
   2256                     values,
   2257                     WHERE_ID,
   2258                     new String[] { String.valueOf(id) });
   2259         }
   2260     }
   2261 
   2262     @Override
   2263     public Bundle call(String method, String arg, Bundle extras) {
   2264         LogUtils.d(TAG, "EmailProvider#call(%s, %s)", method, arg);
   2265 
   2266         // Handle queries for the device friendly name.
   2267         // TODO: This should eventually be a device property, not defined by the app.
   2268         if (TextUtils.equals(method, EmailContent.DEVICE_FRIENDLY_NAME)) {
   2269             final Bundle bundle = new Bundle(1);
   2270             // TODO: For now, just use the model name since we don't yet have a user-supplied name.
   2271             bundle.putString(EmailContent.DEVICE_FRIENDLY_NAME, Build.MODEL);
   2272             return bundle;
   2273         }
   2274 
   2275         // Handle sync status callbacks.
   2276         if (TextUtils.equals(method, SYNC_STATUS_CALLBACK_METHOD)) {
   2277             updateSyncStatus(extras);
   2278             return null;
   2279         }
   2280         if (TextUtils.equals(method, MailboxUtilities.FIX_PARENT_KEYS_METHOD)) {
   2281             fixParentKeys(getDatabase(getContext()));
   2282             return null;
   2283         }
   2284 
   2285         // Handle send & save.
   2286         final Uri accountUri = Uri.parse(arg);
   2287         final long accountId = Long.parseLong(accountUri.getPathSegments().get(1));
   2288 
   2289         Uri messageUri = null;
   2290 
   2291         if (TextUtils.equals(method, UIProvider.AccountCallMethods.SEND_MESSAGE)) {
   2292             messageUri = uiSendDraftMessage(accountId, extras);
   2293             Preferences.getPreferences(getContext()).setLastUsedAccountId(accountId);
   2294         } else if (TextUtils.equals(method, UIProvider.AccountCallMethods.SAVE_MESSAGE)) {
   2295             messageUri = uiSaveDraftMessage(accountId, extras);
   2296         } else if (TextUtils.equals(method, UIProvider.AccountCallMethods.SET_CURRENT_ACCOUNT)) {
   2297             LogUtils.d(TAG, "Unhandled (but expected) Content provider method: %s", method);
   2298         } else {
   2299             LogUtils.wtf(TAG, "Unexpected Content provider method: %s", method);
   2300         }
   2301 
   2302         final Bundle result;
   2303         if (messageUri != null) {
   2304             result = new Bundle(1);
   2305             result.putParcelable(UIProvider.MessageColumns.URI, messageUri);
   2306         } else {
   2307             result = null;
   2308         }
   2309 
   2310         return result;
   2311     }
   2312 
   2313     private static void deleteBodyFiles(final Context c, final long messageId)
   2314             throws IllegalStateException {
   2315         final ContentValues emptyValues = new ContentValues(2);
   2316         emptyValues.putNull(BodyColumns.HTML_CONTENT);
   2317         emptyValues.putNull(BodyColumns.TEXT_CONTENT);
   2318         writeBodyFiles(c, messageId, emptyValues);
   2319     }
   2320 
   2321     /**
   2322      * Writes message bodies to disk, read from a set of ContentValues
   2323      *
   2324      * @param c Context for finding files
   2325      * @param messageId id of message to write body for
   2326      * @param cv {@link ContentValues} containing {@link BodyColumns#HTML_CONTENT} and/or
   2327      *           {@link BodyColumns#TEXT_CONTENT}. Inserting a null or empty value will delete the
   2328      *           associated text or html body file
   2329      * @throws IllegalStateException
   2330      */
   2331     private static void writeBodyFiles(final Context c, final long messageId,
   2332             final ContentValues cv) throws IllegalStateException {
   2333         if (cv.containsKey(BodyColumns.HTML_CONTENT)) {
   2334             final String htmlContent = cv.getAsString(BodyColumns.HTML_CONTENT);
   2335             try {
   2336                 writeBodyFile(c, messageId, "html", htmlContent);
   2337             } catch (final IOException e) {
   2338                 throw new IllegalStateException("IOException while writing html body " +
   2339                         "for message id " + Long.toString(messageId), e);
   2340             }
   2341         }
   2342         if (cv.containsKey(BodyColumns.TEXT_CONTENT)) {
   2343             final String textContent = cv.getAsString(BodyColumns.TEXT_CONTENT);
   2344             try {
   2345                 writeBodyFile(c, messageId, "txt", textContent);
   2346             } catch (final IOException e) {
   2347                 throw new IllegalStateException("IOException while writing text body " +
   2348                         "for message id " + Long.toString(messageId), e);
   2349             }
   2350         }
   2351     }
   2352 
   2353     /**
   2354      * Writes a message body file to disk
   2355      *
   2356      * @param c Context for finding files dir
   2357      * @param messageId id of message to write body for
   2358      * @param ext "html" or "txt"
   2359      * @param content Body content to write to file, or null/empty to delete file
   2360      * @throws IOException
   2361      */
   2362     private static void writeBodyFile(final Context c, final long messageId, final String ext,
   2363             final String content) throws IOException {
   2364         final File textFile = getBodyFile(c, messageId, ext);
   2365         if (TextUtils.isEmpty(content)) {
   2366             if (!textFile.delete()) {
   2367                 LogUtils.v(LogUtils.TAG, "did not delete text body for %d", messageId);
   2368             }
   2369         } else {
   2370             final FileWriter w = new FileWriter(textFile);
   2371             try {
   2372                 w.write(content);
   2373             } finally {
   2374                 w.close();
   2375             }
   2376         }
   2377     }
   2378 
   2379     /**
   2380      * Returns a {@link java.io.File} object pointing to the body content file for the message
   2381      *
   2382      * @param c Context for finding files dir
   2383      * @param messageId id of message to locate
   2384      * @param ext "html" or "txt"
   2385      * @return File ready for operating upon
   2386      */
   2387     protected static File getBodyFile(final Context c, final long messageId, final String ext)
   2388             throws FileNotFoundException {
   2389         if (!TextUtils.equals(ext, "html") && !TextUtils.equals(ext, "txt")) {
   2390             throw new IllegalArgumentException("ext must be one of 'html' or 'txt'");
   2391         }
   2392         long l1 = messageId / 100 % 100;
   2393         long l2 = messageId % 100;
   2394         final File dir = new File(c.getFilesDir(),
   2395                 "body/" + Long.toString(l1) + "/" + Long.toString(l2) + "/");
   2396         if (!dir.isDirectory() && !dir.mkdirs()) {
   2397             throw new FileNotFoundException("Could not create directory for body file");
   2398         }
   2399         return new File(dir, Long.toString(messageId) + "." + ext);
   2400     }
   2401 
   2402     @Override
   2403     public ParcelFileDescriptor openFile(final Uri uri, final String mode)
   2404             throws FileNotFoundException {
   2405         if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
   2406             LogUtils.d(TAG, "EmailProvider.openFile: %s", LogUtils.contentUriToString(TAG, uri));
   2407         }
   2408 
   2409         final int match = findMatch(uri, "openFile");
   2410         switch (match) {
   2411             case ATTACHMENTS_CACHED_FILE_ACCESS:
   2412                 // Parse the cache file path out from the uri
   2413                 final String cachedFilePath =
   2414                         uri.getQueryParameter(Attachment.CACHED_FILE_QUERY_PARAM);
   2415 
   2416                 if (cachedFilePath != null) {
   2417                     // clearCallingIdentity means that the download manager will
   2418                     // check our permissions rather than the permissions of whatever
   2419                     // code is calling us.
   2420                     long binderToken = Binder.clearCallingIdentity();
   2421                     try {
   2422                         LogUtils.d(TAG, "Opening attachment %s", cachedFilePath);
   2423                         return ParcelFileDescriptor.open(
   2424                                 new File(cachedFilePath), ParcelFileDescriptor.MODE_READ_ONLY);
   2425                     } finally {
   2426                         Binder.restoreCallingIdentity(binderToken);
   2427                     }
   2428                 }
   2429                 break;
   2430             case BODY_HTML: {
   2431                 final long messageKey = Long.valueOf(uri.getLastPathSegment());
   2432                 return ParcelFileDescriptor.open(getBodyFile(getContext(), messageKey, "html"),
   2433                         Utilities.parseMode(mode));
   2434             }
   2435             case BODY_TEXT:{
   2436                 final long messageKey = Long.valueOf(uri.getLastPathSegment());
   2437                 return ParcelFileDescriptor.open(getBodyFile(getContext(), messageKey, "txt"),
   2438                         Utilities.parseMode(mode));
   2439             }
   2440         }
   2441 
   2442         throw new FileNotFoundException("unable to open file");
   2443     }
   2444 
   2445 
   2446     /**
   2447      * Returns the base notification URI for the given content type.
   2448      *
   2449      * @param match The type of content that was modified.
   2450      */
   2451     private static Uri getBaseNotificationUri(int match) {
   2452         Uri baseUri = null;
   2453         switch (match) {
   2454             case MESSAGE:
   2455             case MESSAGE_ID:
   2456             case SYNCED_MESSAGE_ID:
   2457                 baseUri = Message.NOTIFIER_URI;
   2458                 break;
   2459             case ACCOUNT:
   2460             case ACCOUNT_ID:
   2461                 baseUri = Account.NOTIFIER_URI;
   2462                 break;
   2463         }
   2464         return baseUri;
   2465     }
   2466 
   2467     /**
   2468      * Sends a change notification to any cursors observers of the given base URI. The final
   2469      * notification URI is dynamically built to contain the specified information. It will be
   2470      * of the format <<baseURI>>/<<op>>/<<id>>; where <<op>> and <<id>> are optional depending
   2471      * upon the given values.
   2472      * NOTE: If <<op>> is specified, notifications for <<baseURI>>/<<id>> will NOT be invoked.
   2473      * If this is necessary, it can be added. However, due to the implementation of
   2474      * {@link ContentObserver}, observers of <<baseURI>> will receive multiple notifications.
   2475      *
   2476      * @param baseUri The base URI to send notifications to. Must be able to take appended IDs.
   2477      * @param op Optional operation to be appended to the URI.
   2478      * @param id If a positive value, the ID to append to the base URI. Otherwise, no ID will be
   2479      *           appended to the base URI.
   2480      */
   2481     private void sendNotifierChange(Uri baseUri, String op, String id) {
   2482         if (baseUri == null) return;
   2483 
   2484         // Append the operation, if specified
   2485         if (op != null) {
   2486             baseUri = baseUri.buildUpon().appendEncodedPath(op).build();
   2487         }
   2488 
   2489         long longId = 0L;
   2490         try {
   2491             longId = Long.valueOf(id);
   2492         } catch (NumberFormatException ignore) {}
   2493         if (longId > 0) {
   2494             notifyUI(baseUri, id);
   2495         } else {
   2496             notifyUI(baseUri, null);
   2497         }
   2498 
   2499         // We want to send the message list changed notification if baseUri is Message.NOTIFIER_URI.
   2500         if (baseUri.equals(Message.NOTIFIER_URI)) {
   2501             sendMessageListDataChangedNotification();
   2502         }
   2503     }
   2504 
   2505     private void sendMessageListDataChangedNotification() {
   2506         final Context context = getContext();
   2507         final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED);
   2508         // Ideally this intent would contain information about which account changed, to limit the
   2509         // updates to that particular account.  Unfortunately, that information is not available in
   2510         // sendNotifierChange().
   2511         context.sendBroadcast(intent);
   2512     }
   2513 
   2514     // We might have more than one thread trying to make its way through applyBatch() so the
   2515     // notification coalescing needs to be thread-local to work correctly.
   2516     private final ThreadLocal<Set<Uri>> mTLBatchNotifications =
   2517             new ThreadLocal<Set<Uri>>();
   2518 
   2519     private Set<Uri> getBatchNotificationsSet() {
   2520         return mTLBatchNotifications.get();
   2521     }
   2522 
   2523     private void setBatchNotificationsSet(Set<Uri> batchNotifications) {
   2524         mTLBatchNotifications.set(batchNotifications);
   2525     }
   2526 
   2527     @Override
   2528     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
   2529             throws OperationApplicationException {
   2530         /**
   2531          * Collect notification URIs to notify at the end of batch processing.
   2532          * These are populated by calls to notifyUI() by way of update(), insert() and delete()
   2533          * calls made in super.applyBatch()
   2534          */
   2535         setBatchNotificationsSet(Sets.<Uri>newHashSet());
   2536         Context context = getContext();
   2537         SQLiteDatabase db = getDatabase(context);
   2538         db.beginTransaction();
   2539         try {
   2540             ContentProviderResult[] results = super.applyBatch(operations);
   2541             db.setTransactionSuccessful();
   2542             return results;
   2543         } finally {
   2544             db.endTransaction();
   2545             final Set<Uri> notifications = getBatchNotificationsSet();
   2546             setBatchNotificationsSet(null);
   2547             for (final Uri uri : notifications) {
   2548                 context.getContentResolver().notifyChange(uri, null);
   2549             }
   2550         }
   2551     }
   2552 
   2553     public static interface EmailAttachmentService {
   2554         /**
   2555          * Notify the service that an attachment has changed.
   2556          */
   2557         void attachmentChanged(final Context context, final long id, final int flags);
   2558     }
   2559 
   2560     private final EmailAttachmentService DEFAULT_ATTACHMENT_SERVICE = new EmailAttachmentService() {
   2561         @Override
   2562         public void attachmentChanged(final Context context, final long id, final int flags) {
   2563             // The default implementation delegates to the real service.
   2564             AttachmentService.attachmentChanged(context, id, flags);
   2565         }
   2566     };
   2567     private EmailAttachmentService mAttachmentService = DEFAULT_ATTACHMENT_SERVICE;
   2568 
   2569     // exposed for testing
   2570     public void injectAttachmentService(final EmailAttachmentService attachmentService) {
   2571         mAttachmentService =
   2572             attachmentService == null ? DEFAULT_ATTACHMENT_SERVICE : attachmentService;
   2573     }
   2574 
   2575     private Cursor notificationQuery(final Uri uri) {
   2576         final SQLiteDatabase db = getDatabase(getContext());
   2577         final String accountId = uri.getLastPathSegment();
   2578 
   2579         final String sql = "SELECT " + MessageColumns.MAILBOX_KEY + ", " +
   2580                 "SUM(CASE " + MessageColumns.FLAG_READ + " WHEN 0 THEN 1 ELSE 0 END), " +
   2581                 "SUM(CASE " + MessageColumns.FLAG_SEEN + " WHEN 0 THEN 1 ELSE 0 END)\n" +
   2582                 "FROM " + Message.TABLE_NAME + "\n" +
   2583                 "WHERE " + MessageColumns.ACCOUNT_KEY + " = ?\n" +
   2584                 "GROUP BY " + MessageColumns.MAILBOX_KEY;
   2585 
   2586         final String[] selectionArgs = {accountId};
   2587 
   2588         return db.rawQuery(sql, selectionArgs);
   2589     }
   2590 
   2591     public Cursor mostRecentMessageQuery(Uri uri) {
   2592         SQLiteDatabase db = getDatabase(getContext());
   2593         String mailboxId = uri.getLastPathSegment();
   2594         return db.rawQuery("select max(_id) from Message where mailboxKey=?",
   2595                 new String[] {mailboxId});
   2596     }
   2597 
   2598     private Cursor getMailboxMessageCount(Uri uri) {
   2599         SQLiteDatabase db = getDatabase(getContext());
   2600         String mailboxId = uri.getLastPathSegment();
   2601         return db.rawQuery("select count(*) from Message where mailboxKey=?",
   2602                 new String[] {mailboxId});
   2603     }
   2604 
   2605     /**
   2606      * Support for UnifiedEmail below
   2607      */
   2608 
   2609     private static final String NOT_A_DRAFT_STRING =
   2610         Integer.toString(UIProvider.DraftType.NOT_A_DRAFT);
   2611 
   2612     private static final String CONVERSATION_FLAGS =
   2613             "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_INCOMING_MEETING_INVITE +
   2614                 ") !=0 THEN " + UIProvider.ConversationFlags.CALENDAR_INVITE +
   2615                 " ELSE 0 END + " +
   2616             "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_FORWARDED +
   2617                 ") !=0 THEN " + UIProvider.ConversationFlags.FORWARDED +
   2618                 " ELSE 0 END + " +
   2619              "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_REPLIED_TO +
   2620                 ") !=0 THEN " + UIProvider.ConversationFlags.REPLIED +
   2621                 " ELSE 0 END";
   2622 
   2623     /**
   2624      * Array of pre-defined account colors (legacy colors from old email app)
   2625      */
   2626     private static final int[] ACCOUNT_COLORS = new int[] {
   2627         0xff71aea7, 0xff621919, 0xff18462f, 0xffbf8e52, 0xff001f79,
   2628         0xffa8afc2, 0xff6b64c4, 0xff738359, 0xff9d50a4
   2629     };
   2630 
   2631     private static final String CONVERSATION_COLOR =
   2632             "@CASE (" + MessageColumns.ACCOUNT_KEY + " - 1) % " + ACCOUNT_COLORS.length +
   2633                     " WHEN 0 THEN " + ACCOUNT_COLORS[0] +
   2634                     " WHEN 1 THEN " + ACCOUNT_COLORS[1] +
   2635                     " WHEN 2 THEN " + ACCOUNT_COLORS[2] +
   2636                     " WHEN 3 THEN " + ACCOUNT_COLORS[3] +
   2637                     " WHEN 4 THEN " + ACCOUNT_COLORS[4] +
   2638                     " WHEN 5 THEN " + ACCOUNT_COLORS[5] +
   2639                     " WHEN 6 THEN " + ACCOUNT_COLORS[6] +
   2640                     " WHEN 7 THEN " + ACCOUNT_COLORS[7] +
   2641                     " WHEN 8 THEN " + ACCOUNT_COLORS[8] +
   2642             " END";
   2643 
   2644     private static final String ACCOUNT_COLOR =
   2645             "@CASE (" + AccountColumns._ID + " - 1) % " + ACCOUNT_COLORS.length +
   2646                     " WHEN 0 THEN " + ACCOUNT_COLORS[0] +
   2647                     " WHEN 1 THEN " + ACCOUNT_COLORS[1] +
   2648                     " WHEN 2 THEN " + ACCOUNT_COLORS[2] +
   2649                     " WHEN 3 THEN " + ACCOUNT_COLORS[3] +
   2650                     " WHEN 4 THEN " + ACCOUNT_COLORS[4] +
   2651                     " WHEN 5 THEN " + ACCOUNT_COLORS[5] +
   2652                     " WHEN 6 THEN " + ACCOUNT_COLORS[6] +
   2653                     " WHEN 7 THEN " + ACCOUNT_COLORS[7] +
   2654                     " WHEN 8 THEN " + ACCOUNT_COLORS[8] +
   2655             " END";
   2656 
   2657     /**
   2658      * Mapping of UIProvider columns to EmailProvider columns for the message list (called the
   2659      * conversation list in UnifiedEmail)
   2660      */
   2661     private static ProjectionMap getMessageListMap() {
   2662         if (sMessageListMap == null) {
   2663             sMessageListMap = ProjectionMap.builder()
   2664                 .add(BaseColumns._ID, MessageColumns._ID)
   2665                 .add(UIProvider.ConversationColumns.URI, uriWithId("uimessage"))
   2666                 .add(UIProvider.ConversationColumns.MESSAGE_LIST_URI, uriWithId("uimessage"))
   2667                 .add(UIProvider.ConversationColumns.SUBJECT, MessageColumns.SUBJECT)
   2668                 .add(UIProvider.ConversationColumns.SNIPPET, MessageColumns.SNIPPET)
   2669                 .add(UIProvider.ConversationColumns.CONVERSATION_INFO, null)
   2670                 .add(UIProvider.ConversationColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP)
   2671                 .add(UIProvider.ConversationColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT)
   2672                 .add(UIProvider.ConversationColumns.NUM_MESSAGES, "1")
   2673                 .add(UIProvider.ConversationColumns.NUM_DRAFTS, "0")
   2674                 .add(UIProvider.ConversationColumns.SENDING_STATE,
   2675                         Integer.toString(ConversationSendingState.OTHER))
   2676                 .add(UIProvider.ConversationColumns.PRIORITY,
   2677                         Integer.toString(ConversationPriority.LOW))
   2678                 .add(UIProvider.ConversationColumns.READ, MessageColumns.FLAG_READ)
   2679                 .add(UIProvider.ConversationColumns.SEEN, MessageColumns.FLAG_SEEN)
   2680                 .add(UIProvider.ConversationColumns.STARRED, MessageColumns.FLAG_FAVORITE)
   2681                 .add(UIProvider.ConversationColumns.FLAGS, CONVERSATION_FLAGS)
   2682                 .add(UIProvider.ConversationColumns.ACCOUNT_URI,
   2683                         uriWithColumn("uiaccount", MessageColumns.ACCOUNT_KEY))
   2684                 .add(UIProvider.ConversationColumns.SENDER_INFO, MessageColumns.FROM_LIST)
   2685                 .add(UIProvider.ConversationColumns.ORDER_KEY, MessageColumns.TIMESTAMP)
   2686                 .build();
   2687         }
   2688         return sMessageListMap;
   2689     }
   2690     private static ProjectionMap sMessageListMap;
   2691 
   2692     /**
   2693      * Generate UIProvider draft type; note the test for "reply all" must come before "reply"
   2694      */
   2695     private static final String MESSAGE_DRAFT_TYPE =
   2696         "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_ORIGINAL +
   2697             ") !=0 THEN " + UIProvider.DraftType.COMPOSE +
   2698         " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_REPLY_ALL +
   2699             ") !=0 THEN " + UIProvider.DraftType.REPLY_ALL +
   2700         " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_REPLY +
   2701             ") !=0 THEN " + UIProvider.DraftType.REPLY +
   2702         " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_FORWARD +
   2703             ") !=0 THEN " + UIProvider.DraftType.FORWARD +
   2704             " ELSE " + UIProvider.DraftType.NOT_A_DRAFT + " END";
   2705 
   2706     private static final String MESSAGE_FLAGS =
   2707             "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_INCOMING_MEETING_INVITE +
   2708             ") !=0 THEN " + UIProvider.MessageFlags.CALENDAR_INVITE +
   2709             " ELSE 0 END";
   2710 
   2711     /**
   2712      * Mapping of UIProvider columns to EmailProvider columns for a detailed message view in
   2713      * UnifiedEmail
   2714      */
   2715     private static ProjectionMap getMessageViewMap() {
   2716         if (sMessageViewMap == null) {
   2717             sMessageViewMap = ProjectionMap.builder()
   2718                 .add(BaseColumns._ID, Message.TABLE_NAME + "." + MessageColumns._ID)
   2719                 .add(UIProvider.MessageColumns.SERVER_ID, SyncColumns.SERVER_ID)
   2720                 .add(UIProvider.MessageColumns.URI, uriWithFQId("uimessage", Message.TABLE_NAME))
   2721                 .add(UIProvider.MessageColumns.CONVERSATION_ID,
   2722                         uriWithFQId("uimessage", Message.TABLE_NAME))
   2723                 .add(UIProvider.MessageColumns.SUBJECT, MessageColumns.SUBJECT)
   2724                 .add(UIProvider.MessageColumns.SNIPPET, MessageColumns.SNIPPET)
   2725                 .add(UIProvider.MessageColumns.FROM, MessageColumns.FROM_LIST)
   2726                 .add(UIProvider.MessageColumns.TO, MessageColumns.TO_LIST)
   2727                 .add(UIProvider.MessageColumns.CC, MessageColumns.CC_LIST)
   2728                 .add(UIProvider.MessageColumns.BCC, MessageColumns.BCC_LIST)
   2729                 .add(UIProvider.MessageColumns.REPLY_TO, MessageColumns.REPLY_TO_LIST)
   2730                 .add(UIProvider.MessageColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP)
   2731                 .add(UIProvider.MessageColumns.BODY_HTML, null) // Loaded in EmailMessageCursor
   2732                 .add(UIProvider.MessageColumns.BODY_TEXT, null) // Loaded in EmailMessageCursor
   2733                 .add(UIProvider.MessageColumns.REF_MESSAGE_ID, "0")
   2734                 .add(UIProvider.MessageColumns.DRAFT_TYPE, NOT_A_DRAFT_STRING)
   2735                 .add(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, "0")
   2736                 .add(UIProvider.MessageColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT)
   2737                 .add(UIProvider.MessageColumns.ATTACHMENT_LIST_URI,
   2738                         uriWithFQId("uiattachments", Message.TABLE_NAME))
   2739                 .add(UIProvider.MessageColumns.ATTACHMENT_BY_CID_URI,
   2740                         uriWithFQId("uiattachmentbycid", Message.TABLE_NAME))
   2741                 .add(UIProvider.MessageColumns.MESSAGE_FLAGS, MESSAGE_FLAGS)
   2742                 .add(UIProvider.MessageColumns.DRAFT_TYPE, MESSAGE_DRAFT_TYPE)
   2743                 .add(UIProvider.MessageColumns.MESSAGE_ACCOUNT_URI,
   2744                         uriWithColumn("uiaccount", MessageColumns.ACCOUNT_KEY))
   2745                 .add(UIProvider.MessageColumns.STARRED, MessageColumns.FLAG_FAVORITE)
   2746                 .add(UIProvider.MessageColumns.READ, MessageColumns.FLAG_READ)
   2747                 .add(UIProvider.MessageColumns.SEEN, MessageColumns.FLAG_SEEN)
   2748                 .add(UIProvider.MessageColumns.SPAM_WARNING_STRING, null)
   2749                 .add(UIProvider.MessageColumns.SPAM_WARNING_LEVEL,
   2750                         Integer.toString(UIProvider.SpamWarningLevel.NO_WARNING))
   2751                 .add(UIProvider.MessageColumns.SPAM_WARNING_LINK_TYPE,
   2752                         Integer.toString(UIProvider.SpamWarningLinkType.NO_LINK))
   2753                 .add(UIProvider.MessageColumns.VIA_DOMAIN, null)
   2754                 .add(UIProvider.MessageColumns.CLIPPED, "0")
   2755                 .add(UIProvider.MessageColumns.PERMALINK, null)
   2756                 .build();
   2757         }
   2758         return sMessageViewMap;
   2759     }
   2760     private static ProjectionMap sMessageViewMap;
   2761 
   2762     /**
   2763      * Generate UIProvider folder capabilities from mailbox flags
   2764      */
   2765     private static final String FOLDER_CAPABILITIES =
   2766         "CASE WHEN (" + MailboxColumns.FLAGS + "&" + Mailbox.FLAG_ACCEPTS_MOVED_MAIL +
   2767             ") !=0 THEN " + UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES +
   2768             " ELSE 0 END";
   2769 
   2770     /**
   2771      * Convert EmailProvider type to UIProvider type
   2772      */
   2773     private static final String FOLDER_TYPE = "CASE " + MailboxColumns.TYPE
   2774             + " WHEN " + Mailbox.TYPE_INBOX   + " THEN " + UIProvider.FolderType.INBOX
   2775             + " WHEN " + Mailbox.TYPE_DRAFTS  + " THEN " + UIProvider.FolderType.DRAFT
   2776             + " WHEN " + Mailbox.TYPE_OUTBOX  + " THEN " + UIProvider.FolderType.OUTBOX
   2777             + " WHEN " + Mailbox.TYPE_SENT    + " THEN " + UIProvider.FolderType.SENT
   2778             + " WHEN " + Mailbox.TYPE_TRASH   + " THEN " + UIProvider.FolderType.TRASH
   2779             + " WHEN " + Mailbox.TYPE_JUNK    + " THEN " + UIProvider.FolderType.SPAM
   2780             + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + UIProvider.FolderType.STARRED
   2781             + " WHEN " + Mailbox.TYPE_UNREAD + " THEN " + UIProvider.FolderType.UNREAD
   2782             + " WHEN " + Mailbox.TYPE_SEARCH + " THEN "
   2783                     + getFolderTypeFromMailboxType(Mailbox.TYPE_SEARCH)
   2784             + " ELSE " + UIProvider.FolderType.DEFAULT + " END";
   2785 
   2786     private static final String FOLDER_ICON = "CASE " + MailboxColumns.TYPE
   2787             + " WHEN " + Mailbox.TYPE_INBOX   + " THEN " + R.drawable.ic_drawer_inbox_24dp
   2788             + " WHEN " + Mailbox.TYPE_DRAFTS  + " THEN " + R.drawable.ic_drawer_drafts_24dp
   2789             + " WHEN " + Mailbox.TYPE_OUTBOX  + " THEN " + R.drawable.ic_drawer_outbox_24dp
   2790             + " WHEN " + Mailbox.TYPE_SENT    + " THEN " + R.drawable.ic_drawer_sent_24dp
   2791             + " WHEN " + Mailbox.TYPE_TRASH   + " THEN " + R.drawable.ic_drawer_trash_24dp
   2792             + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + R.drawable.ic_drawer_starred_24dp
   2793             + " ELSE " + R.drawable.ic_drawer_folder_24dp + " END";
   2794 
   2795     /**
   2796      * Local-only folders set totalCount < 0; such folders should substitute message count for
   2797      * total count.
   2798      * TODO: IMAP and POP don't adhere to this convention yet so for now we force a few types.
   2799      */
   2800     private static final String TOTAL_COUNT = "CASE WHEN "
   2801             + MailboxColumns.TOTAL_COUNT + "<0 OR "
   2802             + MailboxColumns.TYPE + "=" + Mailbox.TYPE_DRAFTS + " OR "
   2803             + MailboxColumns.TYPE + "=" + Mailbox.TYPE_OUTBOX + " OR "
   2804             + MailboxColumns.TYPE + "=" + Mailbox.TYPE_TRASH
   2805             + " THEN " + MailboxColumns.MESSAGE_COUNT
   2806             + " ELSE " + MailboxColumns.TOTAL_COUNT + " END";
   2807 
   2808     private static ProjectionMap getFolderListMap() {
   2809         if (sFolderListMap == null) {
   2810             sFolderListMap = ProjectionMap.builder()
   2811                 .add(BaseColumns._ID, MailboxColumns._ID)
   2812                 .add(UIProvider.FolderColumns.PERSISTENT_ID, MailboxColumns.SERVER_ID)
   2813                 .add(UIProvider.FolderColumns.URI, uriWithId("uifolder"))
   2814                 .add(UIProvider.FolderColumns.NAME, "displayName")
   2815                 .add(UIProvider.FolderColumns.HAS_CHILDREN,
   2816                         MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HAS_CHILDREN)
   2817                 .add(UIProvider.FolderColumns.CAPABILITIES, FOLDER_CAPABILITIES)
   2818                 .add(UIProvider.FolderColumns.SYNC_WINDOW, "3")
   2819                 .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages"))
   2820                 .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("uisubfolders"))
   2821                 .add(UIProvider.FolderColumns.UNREAD_COUNT, MailboxColumns.UNREAD_COUNT)
   2822                 .add(UIProvider.FolderColumns.TOTAL_COUNT, TOTAL_COUNT)
   2823                 .add(UIProvider.FolderColumns.REFRESH_URI, uriWithId(QUERY_UIREFRESH))
   2824                 .add(UIProvider.FolderColumns.SYNC_STATUS, MailboxColumns.UI_SYNC_STATUS)
   2825                 .add(UIProvider.FolderColumns.LAST_SYNC_RESULT, MailboxColumns.UI_LAST_SYNC_RESULT)
   2826                 .add(UIProvider.FolderColumns.TYPE, FOLDER_TYPE)
   2827                 .add(UIProvider.FolderColumns.ICON_RES_ID, FOLDER_ICON)
   2828                 .add(UIProvider.FolderColumns.LOAD_MORE_URI, uriWithId("uiloadmore"))
   2829                 .add(UIProvider.FolderColumns.HIERARCHICAL_DESC, MailboxColumns.HIERARCHICAL_NAME)
   2830                 .add(UIProvider.FolderColumns.PARENT_URI, "case when " + MailboxColumns.PARENT_KEY
   2831                         + "=" + Mailbox.NO_MAILBOX + " then NULL else " +
   2832                         uriWithColumn("uifolder", MailboxColumns.PARENT_KEY) + " end")
   2833                 /**
   2834                  * SELECT group_concat(fromList) FROM
   2835                  * (SELECT fromList FROM message WHERE mailboxKey=? AND flagRead=0
   2836                  *  GROUP BY fromList ORDER BY timestamp DESC)
   2837                  */
   2838                 .add(UIProvider.FolderColumns.UNREAD_SENDERS,
   2839                         "(SELECT group_concat(" + MessageColumns.FROM_LIST + ") FROM " +
   2840                         "(SELECT " + MessageColumns.FROM_LIST + " FROM " + Message.TABLE_NAME +
   2841                         " WHERE " + MessageColumns.MAILBOX_KEY + "=" + Mailbox.TABLE_NAME + "." +
   2842                         MailboxColumns._ID + " AND " + MessageColumns.FLAG_READ + "=0" +
   2843                         " GROUP BY " + MessageColumns.FROM_LIST + " ORDER BY " +
   2844                         MessageColumns.TIMESTAMP + " DESC))")
   2845                 .build();
   2846         }
   2847         return sFolderListMap;
   2848     }
   2849     private static ProjectionMap sFolderListMap;
   2850 
   2851     /**
   2852      * Constructs the map of default entries for accounts. These values can be overridden in
   2853      * {@link #genQueryAccount(String[], String)}.
   2854      */
   2855     private static ProjectionMap getAccountListMap(Context context) {
   2856         if (sAccountListMap == null) {
   2857             final ProjectionMap.Builder builder = ProjectionMap.builder()
   2858                     .add(BaseColumns._ID, AccountColumns._ID)
   2859                     .add(UIProvider.AccountColumns.FOLDER_LIST_URI, uriWithId("uifolders"))
   2860                     .add(UIProvider.AccountColumns.FULL_FOLDER_LIST_URI, uriWithId("uifullfolders"))
   2861                     .add(UIProvider.AccountColumns.ALL_FOLDER_LIST_URI, uriWithId("uiallfolders"))
   2862                     .add(UIProvider.AccountColumns.NAME, AccountColumns.DISPLAY_NAME)
   2863                     .add(UIProvider.AccountColumns.ACCOUNT_MANAGER_NAME,
   2864                             AccountColumns.EMAIL_ADDRESS)
   2865                     .add(UIProvider.AccountColumns.ACCOUNT_ID,
   2866                             AccountColumns.EMAIL_ADDRESS)
   2867                     .add(UIProvider.AccountColumns.SENDER_NAME,
   2868                             AccountColumns.SENDER_NAME)
   2869                     .add(UIProvider.AccountColumns.UNDO_URI,
   2870                             ("'content://" + EmailContent.AUTHORITY + "/uiundo'"))
   2871                     .add(UIProvider.AccountColumns.URI, uriWithId("uiaccount"))
   2872                     .add(UIProvider.AccountColumns.SEARCH_URI, uriWithId("uisearch"))
   2873                             // TODO: Is provider version used?
   2874                     .add(UIProvider.AccountColumns.PROVIDER_VERSION, "1")
   2875                     .add(UIProvider.AccountColumns.SYNC_STATUS, "0")
   2876                     .add(UIProvider.AccountColumns.RECENT_FOLDER_LIST_URI,
   2877                             uriWithId("uirecentfolders"))
   2878                     .add(UIProvider.AccountColumns.DEFAULT_RECENT_FOLDER_LIST_URI,
   2879                             uriWithId("uidefaultrecentfolders"))
   2880                     .add(UIProvider.AccountColumns.SettingsColumns.SIGNATURE,
   2881                             AccountColumns.SIGNATURE)
   2882                     .add(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS,
   2883                             Integer.toString(UIProvider.SnapHeaderValue.ALWAYS))
   2884                     .add(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE, "0")
   2885                     .add(UIProvider.AccountColumns.SettingsColumns.CONVERSATION_VIEW_MODE,
   2886                             Integer.toString(UIProvider.ConversationViewMode.UNDEFINED))
   2887                     .add(UIProvider.AccountColumns.SettingsColumns.VEILED_ADDRESS_PATTERN, null);
   2888 
   2889             final String feedbackUri = context.getString(R.string.email_feedback_uri);
   2890             if (!TextUtils.isEmpty(feedbackUri)) {
   2891                 // This string needs to be in single quotes, as it will be used as a constant
   2892                 // in a sql expression
   2893                 builder.add(UIProvider.AccountColumns.SEND_FEEDBACK_INTENT_URI,
   2894                         "'" + feedbackUri + "'");
   2895             }
   2896 
   2897             final String helpUri = context.getString(R.string.help_uri);
   2898             if (!TextUtils.isEmpty(helpUri)) {
   2899                 // This string needs to be in single quotes, as it will be used as a constant
   2900                 // in a sql expression
   2901                 builder.add(UIProvider.AccountColumns.HELP_INTENT_URI,
   2902                         "'" + helpUri + "'");
   2903             }
   2904 
   2905             sAccountListMap = builder.build();
   2906         }
   2907         return sAccountListMap;
   2908     }
   2909     private static ProjectionMap sAccountListMap;
   2910 
   2911     private static ProjectionMap getQuickResponseMap() {
   2912         if (sQuickResponseMap == null) {
   2913             sQuickResponseMap = ProjectionMap.builder()
   2914                     .add(UIProvider.QuickResponseColumns.TEXT, QuickResponseColumns.TEXT)
   2915                     .add(UIProvider.QuickResponseColumns.URI,
   2916                             "'" + combinedUriString("quickresponse", "") + "'||"
   2917                                     + QuickResponseColumns._ID)
   2918                     .build();
   2919         }
   2920         return sQuickResponseMap;
   2921     }
   2922     private static ProjectionMap sQuickResponseMap;
   2923 
   2924     /**
   2925      * The "ORDER BY" clause for top level folders
   2926      */
   2927     private static final String MAILBOX_ORDER_BY = "CASE " + MailboxColumns.TYPE
   2928         + " WHEN " + Mailbox.TYPE_INBOX   + " THEN 0"
   2929         + " WHEN " + Mailbox.TYPE_DRAFTS  + " THEN 1"
   2930         + " WHEN " + Mailbox.TYPE_OUTBOX  + " THEN 2"
   2931         + " WHEN " + Mailbox.TYPE_SENT    + " THEN 3"
   2932         + " WHEN " + Mailbox.TYPE_TRASH   + " THEN 4"
   2933         + " WHEN " + Mailbox.TYPE_JUNK    + " THEN 5"
   2934         // Other mailboxes (i.e. of Mailbox.TYPE_MAIL) are shown in alphabetical order.
   2935         + " ELSE 10 END"
   2936         + " ," + MailboxColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
   2937 
   2938     /**
   2939      * Mapping of UIProvider columns to EmailProvider columns for a message's attachments
   2940      */
   2941     private static ProjectionMap getAttachmentMap() {
   2942         if (sAttachmentMap == null) {
   2943             sAttachmentMap = ProjectionMap.builder()
   2944                 .add(UIProvider.AttachmentColumns.NAME, AttachmentColumns.FILENAME)
   2945                 .add(UIProvider.AttachmentColumns.SIZE, AttachmentColumns.SIZE)
   2946                 .add(UIProvider.AttachmentColumns.URI, uriWithId("uiattachment"))
   2947                 .add(UIProvider.AttachmentColumns.CONTENT_TYPE, AttachmentColumns.MIME_TYPE)
   2948                 .add(UIProvider.AttachmentColumns.STATE, AttachmentColumns.UI_STATE)
   2949                 .add(UIProvider.AttachmentColumns.DESTINATION, AttachmentColumns.UI_DESTINATION)
   2950                 .add(UIProvider.AttachmentColumns.DOWNLOADED_SIZE,
   2951                         AttachmentColumns.UI_DOWNLOADED_SIZE)
   2952                 .add(UIProvider.AttachmentColumns.CONTENT_URI, AttachmentColumns.CONTENT_URI)
   2953                 .add(UIProvider.AttachmentColumns.FLAGS, AttachmentColumns.FLAGS)
   2954                 .build();
   2955         }
   2956         return sAttachmentMap;
   2957     }
   2958     private static ProjectionMap sAttachmentMap;
   2959 
   2960     /**
   2961      * Generate the SELECT clause using a specified mapping and the original UI projection
   2962      * @param map the ProjectionMap to use for this projection
   2963      * @param projection the projection as sent by UnifiedEmail
   2964      * @return a StringBuilder containing the SELECT expression for a SQLite query
   2965      */
   2966     private static StringBuilder genSelect(ProjectionMap map, String[] projection) {
   2967         return genSelect(map, projection, EMPTY_CONTENT_VALUES);
   2968     }
   2969 
   2970     private static StringBuilder genSelect(ProjectionMap map, String[] projection,
   2971             ContentValues values) {
   2972         final StringBuilder sb = new StringBuilder("SELECT ");
   2973         boolean first = true;
   2974         for (final String column: projection) {
   2975             if (first) {
   2976                 first = false;
   2977             } else {
   2978                 sb.append(',');
   2979             }
   2980             final String val;
   2981             // First look at values; this is an override of default behavior
   2982             if (values.containsKey(column)) {
   2983                 final String value = values.getAsString(column);
   2984                 if (value == null) {
   2985                     val = "NULL AS " + column;
   2986                 } else if (value.startsWith("@")) {
   2987                     val = value.substring(1) + " AS " + column;
   2988                 } else {
   2989                     val = DatabaseUtils.sqlEscapeString(value) + " AS " + column;
   2990                 }
   2991             } else {
   2992                 // Now, get the standard value for the column from our projection map
   2993                 final String mapVal = map.get(column);
   2994                 // If we don't have the column, return "NULL AS <column>", and warn
   2995                 if (mapVal == null) {
   2996                     val = "NULL AS " + column;
   2997                     // Apparently there's a lot of these, so don't spam the log with warnings
   2998                     // LogUtils.w(TAG, "column " + column + " missing from projection map");
   2999                 } else {
   3000                     val = mapVal;
   3001                 }
   3002             }
   3003             sb.append(val);
   3004         }
   3005         return sb;
   3006     }
   3007 
   3008     /**
   3009      * Convenience method to create a Uri string given the "type" of query; we append the type
   3010      * of the query and the id column name (_id)
   3011      *
   3012      * @param type the "type" of the query, as defined by our UriMatcher definitions
   3013      * @return a Uri string
   3014      */
   3015     private static String uriWithId(String type) {
   3016         return uriWithColumn(type, BaseColumns._ID);
   3017     }
   3018 
   3019     /**
   3020      * Convenience method to create a Uri string given the "type" of query; we append the type
   3021      * of the query and the passed in column name
   3022      *
   3023      * @param type the "type" of the query, as defined by our UriMatcher definitions
   3024      * @param columnName the column in the table being queried
   3025      * @return a Uri string
   3026      */
   3027     private static String uriWithColumn(String type, String columnName) {
   3028         return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + columnName;
   3029     }
   3030 
   3031     /**
   3032      * Convenience method to create a Uri string given the "type" of query and the table name to
   3033      * which it applies; we append the type of the query and the fully qualified (FQ) id column
   3034      * (i.e. including the table name); we need this for join queries where _id would otherwise
   3035      * be ambiguous
   3036      *
   3037      * @param type the "type" of the query, as defined by our UriMatcher definitions
   3038      * @param tableName the name of the table whose _id is referred to
   3039      * @return a Uri string
   3040      */
   3041     private static String uriWithFQId(String type, String tableName) {
   3042         return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + tableName + "._id";
   3043     }
   3044 
   3045     // Regex that matches start of img tag. '<(?i)img\s+'.
   3046     private static final Pattern IMG_TAG_START_REGEX = Pattern.compile("<(?i)img\\s+");
   3047 
   3048     /**
   3049      * Class that holds the sqlite query and the attachment (JSON) value (which might be null)
   3050      */
   3051     private static class MessageQuery {
   3052         final String query;
   3053         final String attachmentJson;
   3054 
   3055         MessageQuery(String _query, String _attachmentJson) {
   3056             query = _query;
   3057             attachmentJson = _attachmentJson;
   3058         }
   3059     }
   3060 
   3061     /**
   3062      * Generate the "view message" SQLite query, given a projection from UnifiedEmail
   3063      *
   3064      * @param uiProjection as passed from UnifiedEmail
   3065      * @return the SQLite query to be executed on the EmailProvider database
   3066      */
   3067     private MessageQuery genQueryViewMessage(String[] uiProjection, String id) {
   3068         Context context = getContext();
   3069         long messageId = Long.parseLong(id);
   3070         Message msg = Message.restoreMessageWithId(context, messageId);
   3071         ContentValues values = new ContentValues();
   3072         String attachmentJson = null;
   3073         if (msg != null) {
   3074             Body body = Body.restoreBodyWithMessageId(context, messageId);
   3075             if (body != null) {
   3076                 if (body.mHtmlContent != null) {
   3077                     if (IMG_TAG_START_REGEX.matcher(body.mHtmlContent).find()) {
   3078                         values.put(UIProvider.MessageColumns.EMBEDS_EXTERNAL_RESOURCES, 1);
   3079                     }
   3080                 }
   3081             }
   3082             Address[] fromList = Address.fromHeader(msg.mFrom);
   3083             int autoShowImages = 0;
   3084             final MailPrefs mailPrefs = MailPrefs.get(context);
   3085             for (Address sender : fromList) {
   3086                 final String email = sender.getAddress();
   3087                 if (mailPrefs.getDisplayImagesFromSender(email)) {
   3088                     autoShowImages = 1;
   3089                     break;
   3090                 }
   3091             }
   3092             values.put(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES, autoShowImages);
   3093             // Add attachments...
   3094             Attachment[] atts = Attachment.restoreAttachmentsWithMessageId(context, messageId);
   3095             if (atts.length > 0) {
   3096                 ArrayList<com.android.mail.providers.Attachment> uiAtts =
   3097                         new ArrayList<com.android.mail.providers.Attachment>();
   3098                 for (Attachment att : atts) {
   3099                     // TODO: This code is intended to strip out any inlined attachments (which
   3100                     // would have a non-null contentId) so that they will not display at the bottom
   3101                     // along with the non-inlined attachments.
   3102                     // The problem is that the UI_ATTACHMENTS query does not behave the same way,
   3103                     // which causes crazy formatting.
   3104                     // There is an open question here, should attachments that are inlined
   3105                     // ALSO appear in the list of attachments at the bottom with the non-inlined
   3106                     // attachments?
   3107                     // Either way, the two queries need to behave the same way.
   3108                     // As of now, they will. If we decide to stop this, then we need to enable
   3109                     // the code below, and then also make the UI_ATTACHMENTS query behave
   3110                     // the same way.
   3111 //
   3112 //                    if (att.mContentId != null && att.getContentUri() != null) {
   3113 //                        continue;
   3114 //                    }
   3115                     com.android.mail.providers.Attachment uiAtt =
   3116                             new com.android.mail.providers.Attachment();
   3117                     uiAtt.setName(att.mFileName);
   3118                     uiAtt.setContentType(att.mMimeType);
   3119                     uiAtt.size = (int) att.mSize;
   3120                     uiAtt.uri = uiUri("uiattachment", att.mId);
   3121                     uiAtt.flags = att.mFlags;
   3122                     uiAtts.add(uiAtt);
   3123                 }
   3124                 values.put(UIProvider.MessageColumns.ATTACHMENTS, "@?"); // @ for literal
   3125                 attachmentJson = com.android.mail.providers.Attachment.toJSONArray(uiAtts);
   3126             }
   3127             if (msg.mDraftInfo != 0) {
   3128                 values.put(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT,
   3129                         (msg.mDraftInfo & Message.DRAFT_INFO_APPEND_REF_MESSAGE) != 0 ? 1 : 0);
   3130                 values.put(UIProvider.MessageColumns.QUOTE_START_POS,
   3131                         msg.mDraftInfo & Message.DRAFT_INFO_QUOTE_POS_MASK);
   3132             }
   3133             if ((msg.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) {
   3134                 values.put(UIProvider.MessageColumns.EVENT_INTENT_URI,
   3135                         "content://ui.email2.android.com/event/" + msg.mId);
   3136             }
   3137             /**
   3138              * HACK: override the attachment uri to contain a query parameter
   3139              * This forces the message footer to reload the attachment display when the message is
   3140              * fully loaded.
   3141              */
   3142             final Uri attachmentListUri = uiUri("uiattachments", messageId).buildUpon()
   3143                     .appendQueryParameter("MessageLoaded",
   3144                             msg.mFlagLoaded == Message.FLAG_LOADED_COMPLETE ? "true" : "false")
   3145                     .build();
   3146             values.put(UIProvider.MessageColumns.ATTACHMENT_LIST_URI, attachmentListUri.toString());
   3147         }
   3148         StringBuilder sb = genSelect(getMessageViewMap(), uiProjection, values);
   3149         sb.append(" FROM " + Message.TABLE_NAME + " LEFT JOIN " + Body.TABLE_NAME +
   3150                 " ON " + BodyColumns.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." +
   3151                         MessageColumns._ID +
   3152                 " WHERE " + Message.TABLE_NAME + "." + MessageColumns._ID + "=?");
   3153         String sql = sb.toString();
   3154         return new MessageQuery(sql, attachmentJson);
   3155     }
   3156 
   3157     private static void appendConversationInfoColumns(final StringBuilder stringBuilder) {
   3158         // TODO(skennedy) These columns are needed for the respond call for ConversationInfo :(
   3159         // There may be a better way to do this, but since the projection is specified by the
   3160         // unified UI code, it can't ask for these columns.
   3161         stringBuilder.append(',').append(MessageColumns.DISPLAY_NAME)
   3162                 .append(',').append(MessageColumns.FROM_LIST)
   3163                 .append(',').append(MessageColumns.TO_LIST);
   3164     }
   3165 
   3166     /**
   3167      * Generate the "message list" SQLite query, given a projection from UnifiedEmail
   3168      *
   3169      * @param uiProjection as passed from UnifiedEmail
   3170      * @param unseenOnly <code>true</code> to only return unseen messages
   3171      * @return the SQLite query to be executed on the EmailProvider database
   3172      */
   3173     private static String genQueryMailboxMessages(String[] uiProjection, final boolean unseenOnly) {
   3174         StringBuilder sb = genSelect(getMessageListMap(), uiProjection);
   3175         appendConversationInfoColumns(sb);
   3176         sb.append(" FROM " + Message.TABLE_NAME + " WHERE " +
   3177                 Message.FLAG_LOADED_SELECTION + " AND " +
   3178                 MessageColumns.MAILBOX_KEY + "=? ");
   3179         if (unseenOnly) {
   3180             sb.append("AND ").append(MessageColumns.FLAG_SEEN).append(" = 0 ");
   3181             sb.append("AND ").append(MessageColumns.FLAG_READ).append(" = 0 ");
   3182         }
   3183         sb.append("ORDER BY " + MessageColumns.TIMESTAMP + " DESC ");
   3184         sb.append("LIMIT " + UIProvider.CONVERSATION_PROJECTION_QUERY_CURSOR_WINDOW_LIMIT);
   3185         return sb.toString();
   3186     }
   3187 
   3188     /**
   3189      * Generate various virtual mailbox SQLite queries, given a projection from UnifiedEmail
   3190      *
   3191      * @param uiProjection as passed from UnifiedEmail
   3192      * @param mailboxId the id of the virtual mailbox
   3193      * @param unseenOnly <code>true</code> to only return unseen messages
   3194      * @return the SQLite query to be executed on the EmailProvider database
   3195      */
   3196     private static Cursor getVirtualMailboxMessagesCursor(SQLiteDatabase db, String[] uiProjection,
   3197             long mailboxId, final boolean unseenOnly) {
   3198         ContentValues values = new ContentValues();
   3199         values.put(UIProvider.ConversationColumns.COLOR, CONVERSATION_COLOR);
   3200         final int virtualMailboxId = getVirtualMailboxType(mailboxId);
   3201         final String[] selectionArgs;
   3202         StringBuilder sb = genSelect(getMessageListMap(), uiProjection, values);
   3203         appendConversationInfoColumns(sb);
   3204         sb.append(" FROM " + Message.TABLE_NAME + " WHERE " +
   3205                 Message.FLAG_LOADED_SELECTION + " AND ");
   3206         if (isCombinedMailbox(mailboxId)) {
   3207             if (unseenOnly) {
   3208                 sb.append(MessageColumns.FLAG_SEEN).append("=0 AND ");
   3209                 sb.append(MessageColumns.FLAG_READ).append("=0 AND ");
   3210             }
   3211             selectionArgs = null;
   3212         } else {
   3213             if (virtualMailboxId == Mailbox.TYPE_INBOX) {
   3214                 throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId);
   3215             }
   3216             sb.append(MessageColumns.ACCOUNT_KEY).append("=? AND ");
   3217             selectionArgs = new String[]{getVirtualMailboxAccountIdString(mailboxId)};
   3218         }
   3219         switch (getVirtualMailboxType(mailboxId)) {
   3220             case Mailbox.TYPE_INBOX:
   3221                 sb.append(MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns._ID +
   3222                         " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE +
   3223                         "=" + Mailbox.TYPE_INBOX + ")");
   3224                 break;
   3225             case Mailbox.TYPE_STARRED:
   3226                 sb.append(MessageColumns.FLAG_FAVORITE + "=1");
   3227                 break;
   3228             case Mailbox.TYPE_UNREAD:
   3229                 sb.append(MessageColumns.FLAG_READ + "=0 AND " + MessageColumns.MAILBOX_KEY +
   3230                         " NOT IN (SELECT " + MailboxColumns._ID + " FROM " + Mailbox.TABLE_NAME +
   3231                         " WHERE " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_TRASH + ")");
   3232                 break;
   3233             default:
   3234                 throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId);
   3235         }
   3236         sb.append(" ORDER BY " + MessageColumns.TIMESTAMP + " DESC");
   3237         return db.rawQuery(sb.toString(), selectionArgs);
   3238     }
   3239 
   3240     /**
   3241      * Generate the "message list" SQLite query, given a projection from UnifiedEmail
   3242      *
   3243      * @param uiProjection as passed from UnifiedEmail
   3244      * @return the SQLite query to be executed on the EmailProvider database
   3245      */
   3246     private static String genQueryConversation(String[] uiProjection) {
   3247         StringBuilder sb = genSelect(getMessageListMap(), uiProjection);
   3248         sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + MessageColumns._ID + "=?");
   3249         return sb.toString();
   3250     }
   3251 
   3252     /**
   3253      * Generate the "top level folder list" SQLite query, given a projection from UnifiedEmail
   3254      *
   3255      * @param uiProjection as passed from UnifiedEmail
   3256      * @return the SQLite query to be executed on the EmailProvider database
   3257      */
   3258     private static String genQueryAccountMailboxes(String[] uiProjection) {
   3259         StringBuilder sb = genSelect(getFolderListMap(), uiProjection);
   3260         sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY +
   3261                 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL +
   3262                 " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH +
   3263                 " AND " + MailboxColumns.PARENT_KEY + " < 0 ORDER BY ");
   3264         sb.append(MAILBOX_ORDER_BY);
   3265         return sb.toString();
   3266     }
   3267 
   3268     /**
   3269      * Generate the "all folders" SQLite query, given a projection from UnifiedEmail.  The list is
   3270      * sorted by the name as it appears in a hierarchical listing
   3271      *
   3272      * @param uiProjection as passed from UnifiedEmail
   3273      * @return the SQLite query to be executed on the EmailProvider database
   3274      */
   3275     private static String genQueryAccountAllMailboxes(String[] uiProjection) {
   3276         StringBuilder sb = genSelect(getFolderListMap(), uiProjection);
   3277         // Use a derived column to choose either hierarchicalName or displayName
   3278         sb.append(", case when " + MailboxColumns.HIERARCHICAL_NAME + " is null then " +
   3279                 MailboxColumns.DISPLAY_NAME + " else " + MailboxColumns.HIERARCHICAL_NAME +
   3280                 " end as h_name");
   3281         // Order by the derived column
   3282         sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY +
   3283                 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL +
   3284                 " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH +
   3285                 " ORDER BY h_name");
   3286         return sb.toString();
   3287     }
   3288 
   3289     /**
   3290      * Generate the "recent folder list" SQLite query, given a projection from UnifiedEmail
   3291      *
   3292      * @param uiProjection as passed from UnifiedEmail
   3293      * @return the SQLite query to be executed on the EmailProvider database
   3294      */
   3295     private static String genQueryRecentMailboxes(String[] uiProjection) {
   3296         StringBuilder sb = genSelect(getFolderListMap(), uiProjection);
   3297         sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY +
   3298                 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL +
   3299                 " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH +
   3300                 " AND " + MailboxColumns.PARENT_KEY + " < 0 AND " +
   3301                 MailboxColumns.LAST_TOUCHED_TIME + " > 0 ORDER BY " +
   3302                 MailboxColumns.LAST_TOUCHED_TIME + " DESC");
   3303         return sb.toString();
   3304     }
   3305 
   3306     private int getFolderCapabilities(EmailServiceInfo info, int mailboxType, long mailboxId) {
   3307         // Special case for Search folders: only permit delete, do not try to give any other caps.
   3308         if (mailboxType == Mailbox.TYPE_SEARCH) {
   3309             return UIProvider.FolderCapabilities.DELETE;
   3310         }
   3311 
   3312         // All folders support delete, except drafts.
   3313         int caps = 0;
   3314         if (mailboxType != Mailbox.TYPE_DRAFTS) {
   3315             caps = UIProvider.FolderCapabilities.DELETE;
   3316         }
   3317         if (info != null && info.offerLookback) {
   3318             // Protocols supporting lookback support settings
   3319             caps |= UIProvider.FolderCapabilities.SUPPORTS_SETTINGS;
   3320         }
   3321 
   3322         if (mailboxType == Mailbox.TYPE_MAIL || mailboxType == Mailbox.TYPE_TRASH ||
   3323                 mailboxType == Mailbox.TYPE_JUNK || mailboxType == Mailbox.TYPE_INBOX) {
   3324             // If the mailbox can accept moved mail, report that as well
   3325             caps |= UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES;
   3326             caps |= UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION;
   3327         }
   3328 
   3329         // For trash, we don't allow undo
   3330         if (mailboxType == Mailbox.TYPE_TRASH) {
   3331             caps =  UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES |
   3332                     UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION |
   3333                     UIProvider.FolderCapabilities.DELETE |
   3334                     UIProvider.FolderCapabilities.DELETE_ACTION_FINAL;
   3335         }
   3336         if (isVirtualMailbox(mailboxId)) {
   3337             caps |= UIProvider.FolderCapabilities.IS_VIRTUAL;
   3338         }
   3339 
   3340         // If we don't know the protocol or the protocol doesn't support it, don't allow moving
   3341         // messages
   3342         if (info == null || !info.offerMoveTo) {
   3343             caps &= ~UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES &
   3344                     ~UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION &
   3345                     ~UIProvider.FolderCapabilities.ALLOWS_MOVE_TO_INBOX;
   3346         }
   3347 
   3348         // If the mailbox stores outgoing mail, show recipients instead of senders
   3349         // (however the Drafts folder shows neither senders nor recipients... just the word "Draft")
   3350         if (mailboxType == Mailbox.TYPE_OUTBOX || mailboxType == Mailbox.TYPE_SENT) {
   3351             caps |= UIProvider.FolderCapabilities.SHOW_RECIPIENTS;
   3352         }
   3353 
   3354         return caps;
   3355     }
   3356 
   3357     /**
   3358      * Generate a "single mailbox" SQLite query, given a projection from UnifiedEmail
   3359      *
   3360      * @param uiProjection as passed from UnifiedEmail
   3361      * @return the SQLite query to be executed on the EmailProvider database
   3362      */
   3363     private String genQueryMailbox(String[] uiProjection, String id) {
   3364         long mailboxId = Long.parseLong(id);
   3365         ContentValues values = new ContentValues(3);
   3366         if (mSearchParams != null && mailboxId == mSearchParams.mSearchMailboxId) {
   3367             // "load more" is valid for search results
   3368             values.put(UIProvider.FolderColumns.LOAD_MORE_URI,
   3369                     uiUriString("uiloadmore", mailboxId));
   3370             values.put(UIProvider.FolderColumns.CAPABILITIES, UIProvider.FolderCapabilities.DELETE);
   3371         } else {
   3372             Context context = getContext();
   3373             Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
   3374             // Make sure we can't get NPE if mailbox has disappeared (the result will end up moot)
   3375             if (mailbox != null) {
   3376                 String protocol = Account.getProtocol(context, mailbox.mAccountKey);
   3377                 EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol);
   3378                 // All folders support delete
   3379                 if (info != null && info.offerLoadMore) {
   3380                     // "load more" is valid for protocols not supporting "lookback"
   3381                     values.put(UIProvider.FolderColumns.LOAD_MORE_URI,
   3382                             uiUriString("uiloadmore", mailboxId));
   3383                 }
   3384                 values.put(UIProvider.FolderColumns.CAPABILITIES,
   3385                         getFolderCapabilities(info, mailbox.mType, mailboxId));
   3386                 // The persistent id is used to form a filename, so we must ensure that it doesn't
   3387                 // include illegal characters (such as '/'). Only perform the encoding if this
   3388                 // query wants the persistent id.
   3389                 boolean shouldEncodePersistentId = false;
   3390                 if (uiProjection == null) {
   3391                     shouldEncodePersistentId = true;
   3392                 } else {
   3393                     for (final String column : uiProjection) {
   3394                         if (TextUtils.equals(column, UIProvider.FolderColumns.PERSISTENT_ID)) {
   3395                             shouldEncodePersistentId = true;
   3396                             break;
   3397                         }
   3398                     }
   3399                 }
   3400                 if (shouldEncodePersistentId) {
   3401                     values.put(UIProvider.FolderColumns.PERSISTENT_ID,
   3402                             Base64.encodeToString(mailbox.mServerId.getBytes(),
   3403                                     Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING));
   3404                 }
   3405              }
   3406         }
   3407         StringBuilder sb = genSelect(getFolderListMap(), uiProjection, values);
   3408         sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns._ID + "=?");
   3409         return sb.toString();
   3410     }
   3411 
   3412     public static final String LEGACY_AUTHORITY = "ui.email.android.com";
   3413     private static final Uri BASE_EXTERNAL_URI = Uri.parse("content://" + LEGACY_AUTHORITY);
   3414 
   3415     private static final Uri BASE_EXTERAL_URI2 = Uri.parse("content://ui.email2.android.com");
   3416 
   3417     private static String getExternalUriString(String segment, String account) {
   3418         return BASE_EXTERNAL_URI.buildUpon().appendPath(segment)
   3419                 .appendQueryParameter("account", account).build().toString();
   3420     }
   3421 
   3422     private static String getExternalUriStringEmail2(String segment, String account) {
   3423         return BASE_EXTERAL_URI2.buildUpon().appendPath(segment)
   3424                 .appendQueryParameter("account", account).build().toString();
   3425     }
   3426 
   3427     private static String getBits(int bitField) {
   3428         StringBuilder sb = new StringBuilder(" ");
   3429         for (int i = 0; i < 32; i++, bitField >>= 1) {
   3430             if ((bitField & 1) != 0) {
   3431                 sb.append(i)
   3432                         .append(" ");
   3433             }
   3434         }
   3435         return sb.toString();
   3436     }
   3437 
   3438     private static int getCapabilities(Context context, final Account account) {
   3439         if (account == null) {
   3440             return 0;
   3441         }
   3442         // Account capabilities are based on protocol -- different protocols (and, for EAS,
   3443         // different protocol versions) support different feature sets.
   3444         final String protocol = account.getProtocol(context);
   3445         int capabilities;
   3446         if (TextUtils.equals(context.getString(R.string.protocol_imap), protocol) ||
   3447                 TextUtils.equals(context.getString(R.string.protocol_legacy_imap), protocol)) {
   3448             capabilities = AccountCapabilities.SYNCABLE_FOLDERS |
   3449                     AccountCapabilities.SERVER_SEARCH |
   3450                     AccountCapabilities.FOLDER_SERVER_SEARCH |
   3451                     AccountCapabilities.UNDO |
   3452                     AccountCapabilities.DISCARD_CONVERSATION_DRAFTS;
   3453         } else if (TextUtils.equals(context.getString(R.string.protocol_pop3), protocol)) {
   3454             capabilities = AccountCapabilities.UNDO |
   3455                     AccountCapabilities.DISCARD_CONVERSATION_DRAFTS;
   3456         } else if (TextUtils.equals(context.getString(R.string.protocol_eas), protocol)) {
   3457             final String easVersion = account.mProtocolVersion;
   3458             double easVersionDouble = 2.5D;
   3459             if (easVersion != null) {
   3460                 try {
   3461                     easVersionDouble = Double.parseDouble(easVersion);
   3462                 } catch (final NumberFormatException e) {
   3463                     // Use the default (lowest) set of capabilities.
   3464                 }
   3465             }
   3466             if (easVersionDouble >= 12.0D) {
   3467                 capabilities = AccountCapabilities.SYNCABLE_FOLDERS |
   3468                         AccountCapabilities.SERVER_SEARCH |
   3469                         AccountCapabilities.FOLDER_SERVER_SEARCH |
   3470                         AccountCapabilities.SMART_REPLY |
   3471                         AccountCapabilities.UNDO |
   3472                         AccountCapabilities.DISCARD_CONVERSATION_DRAFTS;
   3473             } else {
   3474                 capabilities = AccountCapabilities.SYNCABLE_FOLDERS |
   3475                         AccountCapabilities.SMART_REPLY |
   3476                         AccountCapabilities.UNDO |
   3477                         AccountCapabilities.DISCARD_CONVERSATION_DRAFTS;
   3478             }
   3479         } else {
   3480             LogUtils.w(TAG, "Unknown protocol for account %d", account.getId());
   3481             return 0;
   3482         }
   3483         LogUtils.d(TAG, "getCapabilities() for %d (protocol %s): 0x%x %s", account.getId(), protocol,
   3484                 capabilities, getBits(capabilities));
   3485 
   3486         // If the configuration states that feedback is supported, add that capability
   3487         final Resources res = context.getResources();
   3488         if (res.getBoolean(R.bool.feedback_supported)) {
   3489             capabilities |= AccountCapabilities.SEND_FEEDBACK;
   3490         }
   3491 
   3492         // If we can find a help URL then add the Help capability
   3493         if (!TextUtils.isEmpty(context.getResources().getString(R.string.help_uri))) {
   3494             capabilities |= AccountCapabilities.HELP_CONTENT;
   3495         }
   3496 
   3497         capabilities |= AccountCapabilities.EMPTY_TRASH;
   3498 
   3499         // TODO: Should this be stored per-account, or some other mechanism?
   3500         capabilities |= AccountCapabilities.NESTED_FOLDERS;
   3501 
   3502         // the client is permitted to sanitize HTML emails for all Email accounts
   3503         capabilities |= AccountCapabilities.CLIENT_SANITIZED_HTML;
   3504 
   3505         return capabilities;
   3506     }
   3507 
   3508     /**
   3509      * Generate a "single account" SQLite query, given a projection from UnifiedEmail
   3510      *
   3511      * @param uiProjection as passed from UnifiedEmail
   3512      * @param id account row ID
   3513      * @return the SQLite query to be executed on the EmailProvider database
   3514      */
   3515     private String genQueryAccount(String[] uiProjection, String id) {
   3516         final ContentValues values = new ContentValues();
   3517         final long accountId = Long.parseLong(id);
   3518         final Context context = getContext();
   3519 
   3520         EmailServiceInfo info = null;
   3521 
   3522         // TODO: If uiProjection is null, this will NPE. We should do everything here if it's null.
   3523         final Set<String> projectionColumns = ImmutableSet.copyOf(uiProjection);
   3524 
   3525         final Account account = Account.restoreAccountWithId(context, accountId);
   3526         if (account == null) {
   3527             LogUtils.d(TAG, "Account %d not found during genQueryAccount", accountId);
   3528         }
   3529         if (projectionColumns.contains(UIProvider.AccountColumns.CAPABILITIES)) {
   3530             // Get account capabilities from the service
   3531             values.put(UIProvider.AccountColumns.CAPABILITIES,
   3532                     (account == null ? 0 : getCapabilities(context, account)));
   3533         }
   3534         if (projectionColumns.contains(UIProvider.AccountColumns.SETTINGS_INTENT_URI)) {
   3535             values.put(UIProvider.AccountColumns.SETTINGS_INTENT_URI,
   3536                     getExternalUriString("settings", id));
   3537         }
   3538         if (projectionColumns.contains(UIProvider.AccountColumns.COMPOSE_URI)) {
   3539             values.put(UIProvider.AccountColumns.COMPOSE_URI,
   3540                     getExternalUriStringEmail2("compose", id));
   3541         }
   3542         if (projectionColumns.contains(UIProvider.AccountColumns.REAUTHENTICATION_INTENT_URI)) {
   3543             values.put(UIProvider.AccountColumns.REAUTHENTICATION_INTENT_URI,
   3544                     getIncomingSettingsUri(accountId).toString());
   3545         }
   3546         if (projectionColumns.contains(UIProvider.AccountColumns.MIME_TYPE)) {
   3547             values.put(UIProvider.AccountColumns.MIME_TYPE, EMAIL_APP_MIME_TYPE);
   3548         }
   3549         if (projectionColumns.contains(UIProvider.AccountColumns.COLOR)) {
   3550             values.put(UIProvider.AccountColumns.COLOR, ACCOUNT_COLOR);
   3551         }
   3552 
   3553         // TODO: if we're getting the values out of MailPrefs then we don't need to be passing the
   3554         // values this way
   3555         final MailPrefs mailPrefs = MailPrefs.get(getContext());
   3556         if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) {
   3557             values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE,