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,
   3558                     mailPrefs.getConfirmDelete() ? "1" : "0");
   3559         }
   3560         if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) {
   3561             values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND,
   3562                     mailPrefs.getConfirmSend() ? "1" : "0");
   3563         }
   3564         if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.SWIPE)) {
   3565             values.put(UIProvider.AccountColumns.SettingsColumns.SWIPE,
   3566                     mailPrefs.getConversationListSwipeActionInteger(false));
   3567         }
   3568         if (projectionColumns.contains(
   3569                 UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)) {
   3570             values.put(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON,
   3571                     getConversationListIcon(mailPrefs));
   3572         }
   3573         if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)) {
   3574             values.put(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE,
   3575                     Integer.toString(mailPrefs.getAutoAdvanceMode()));
   3576         }
   3577         // Set default inbox, if we've got an inbox; otherwise, say initial sync needed
   3578         final long inboxMailboxId =
   3579                 Mailbox.findMailboxOfType(context, accountId, Mailbox.TYPE_INBOX);
   3580         if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX) &&
   3581                 inboxMailboxId != Mailbox.NO_MAILBOX) {
   3582             values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX,
   3583                     uiUriString("uifolder", inboxMailboxId));
   3584         } else {
   3585             values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX,
   3586                     uiUriString("uiinbox", accountId));
   3587         }
   3588         if (projectionColumns.contains(
   3589                 UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME) &&
   3590                 inboxMailboxId != Mailbox.NO_MAILBOX) {
   3591             values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME,
   3592                     Mailbox.getDisplayName(context, inboxMailboxId));
   3593         }
   3594         if (projectionColumns.contains(UIProvider.AccountColumns.SYNC_STATUS)) {
   3595             if (inboxMailboxId != Mailbox.NO_MAILBOX) {
   3596                 values.put(UIProvider.AccountColumns.SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC);
   3597             } else {
   3598                 values.put(UIProvider.AccountColumns.SYNC_STATUS,
   3599                         UIProvider.SyncStatus.INITIAL_SYNC_NEEDED);
   3600             }
   3601         }
   3602         if (projectionColumns.contains(UIProvider.AccountColumns.UPDATE_SETTINGS_URI)) {
   3603             values.put(UIProvider.AccountColumns.UPDATE_SETTINGS_URI,
   3604                     uiUriString("uiacctsettings", -1));
   3605         }
   3606         if (projectionColumns.contains(UIProvider.AccountColumns.ENABLE_MESSAGE_TRANSFORMS)) {
   3607             // Email is now sanitized, which grants the ability to inject beautifying javascript.
   3608             values.put(UIProvider.AccountColumns.ENABLE_MESSAGE_TRANSFORMS, 1);
   3609         }
   3610         if (projectionColumns.contains(UIProvider.AccountColumns.SECURITY_HOLD)) {
   3611             final int hold = ((account != null &&
   3612                     ((account.getFlags() & Account.FLAGS_SECURITY_HOLD) == 0)) ? 0 : 1);
   3613             values.put(UIProvider.AccountColumns.SECURITY_HOLD, hold);
   3614         }
   3615         if (projectionColumns.contains(UIProvider.AccountColumns.ACCOUNT_SECURITY_URI)) {
   3616             values.put(UIProvider.AccountColumns.ACCOUNT_SECURITY_URI,
   3617                     (account == null ? "" : AccountSecurity.getUpdateSecurityUri(
   3618                             account.getId(), true).toString()));
   3619         }
   3620         if (projectionColumns.contains(
   3621                 UIProvider.AccountColumns.SettingsColumns.IMPORTANCE_MARKERS_ENABLED)) {
   3622             // Email doesn't support priority inbox, so always state importance markers disabled.
   3623             values.put(UIProvider.AccountColumns.SettingsColumns.IMPORTANCE_MARKERS_ENABLED, "0");
   3624         }
   3625         if (projectionColumns.contains(
   3626                 UIProvider.AccountColumns.SettingsColumns.SHOW_CHEVRONS_ENABLED)) {
   3627             // Email doesn't support priority inbox, so always state show chevrons disabled.
   3628             values.put(UIProvider.AccountColumns.SettingsColumns.SHOW_CHEVRONS_ENABLED, "0");
   3629         }
   3630         if (projectionColumns.contains(
   3631                 UIProvider.AccountColumns.SettingsColumns.SETUP_INTENT_URI)) {
   3632             // Set the setup intent if needed
   3633             // TODO We should clarify/document the trash/setup relationship
   3634             long trashId = Mailbox.findMailboxOfType(context, accountId, Mailbox.TYPE_TRASH);
   3635             if (trashId == Mailbox.NO_MAILBOX) {
   3636                 info = EmailServiceUtils.getServiceInfoForAccount(context, accountId);
   3637                 if (info != null && info.requiresSetup) {
   3638                     values.put(UIProvider.AccountColumns.SettingsColumns.SETUP_INTENT_URI,
   3639                             getExternalUriString("setup", id));
   3640                 }
   3641             }
   3642         }
   3643         if (projectionColumns.contains(UIProvider.AccountColumns.TYPE)) {
   3644             final String type;
   3645             if (info == null) {
   3646                 info = EmailServiceUtils.getServiceInfoForAccount(context, accountId);
   3647             }
   3648             if (info != null) {
   3649                 type = info.accountType;
   3650             } else {
   3651                 type = "unknown";
   3652             }
   3653 
   3654             values.put(UIProvider.AccountColumns.TYPE, type);
   3655         }
   3656         if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX) &&
   3657                 inboxMailboxId != Mailbox.NO_MAILBOX) {
   3658             values.put(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX,
   3659                     uiUriString("uifolder", inboxMailboxId));
   3660         }
   3661         if (projectionColumns.contains(UIProvider.AccountColumns.SYNC_AUTHORITY)) {
   3662             values.put(UIProvider.AccountColumns.SYNC_AUTHORITY, EmailContent.AUTHORITY);
   3663         }
   3664         if (projectionColumns.contains(UIProvider.AccountColumns.QUICK_RESPONSE_URI)) {
   3665             values.put(UIProvider.AccountColumns.QUICK_RESPONSE_URI,
   3666                     combinedUriString("quickresponse/account", id));
   3667         }
   3668         if (projectionColumns.contains(UIProvider.AccountColumns.SETTINGS_FRAGMENT_CLASS)) {
   3669             values.put(UIProvider.AccountColumns.SETTINGS_FRAGMENT_CLASS,
   3670                     PREFERENCE_FRAGMENT_CLASS_NAME);
   3671         }
   3672         if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)) {
   3673             values.put(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR,
   3674                     mailPrefs.getDefaultReplyAll()
   3675                             ? UIProvider.DefaultReplyBehavior.REPLY_ALL
   3676                             : UIProvider.DefaultReplyBehavior.REPLY);
   3677         }
   3678         if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.SHOW_IMAGES)) {
   3679             values.put(UIProvider.AccountColumns.SettingsColumns.SHOW_IMAGES,
   3680                     Settings.ShowImages.ASK_FIRST);
   3681         }
   3682 
   3683         final StringBuilder sb = genSelect(getAccountListMap(getContext()), uiProjection, values);
   3684         sb.append(" FROM " + Account.TABLE_NAME + " WHERE " + AccountColumns._ID + "=?");
   3685         return sb.toString();
   3686     }
   3687 
   3688     /**
   3689      * Generate a Uri string for a combined mailbox uri
   3690      * @param type the uri command type (e.g. "uimessages")
   3691      * @param id the id of the item (e.g. an account, mailbox, or message id)
   3692      * @return a Uri string
   3693      */
   3694     private static String combinedUriString(String type, String id) {
   3695         return "content://" + EmailContent.AUTHORITY + "/" + type + "/" + id;
   3696     }
   3697 
   3698     public static final long COMBINED_ACCOUNT_ID = 0x10000000;
   3699 
   3700     /**
   3701      * Generate an id for a combined mailbox of a given type
   3702      * @param type the mailbox type for the combined mailbox
   3703      * @return the id, as a String
   3704      */
   3705     private static String combinedMailboxId(int type) {
   3706         return Long.toString(Account.ACCOUNT_ID_COMBINED_VIEW + type);
   3707     }
   3708 
   3709     public static long getVirtualMailboxId(long accountId, int type) {
   3710         return (accountId << 32) + type;
   3711     }
   3712 
   3713     private static boolean isVirtualMailbox(long mailboxId) {
   3714         return mailboxId >= 0x100000000L;
   3715     }
   3716 
   3717     private static boolean isCombinedMailbox(long mailboxId) {
   3718         return (mailboxId >> 32) == COMBINED_ACCOUNT_ID;
   3719     }
   3720 
   3721     private static long getVirtualMailboxAccountId(long mailboxId) {
   3722         return mailboxId >> 32;
   3723     }
   3724 
   3725     private static String getVirtualMailboxAccountIdString(long mailboxId) {
   3726         return Long.toString(mailboxId >> 32);
   3727     }
   3728 
   3729     private static int getVirtualMailboxType(long mailboxId) {
   3730         return (int)(mailboxId & 0xF);
   3731     }
   3732 
   3733     private void addCombinedAccountRow(MatrixCursor mc) {
   3734         final long lastUsedAccountId =
   3735                 Preferences.getPreferences(getContext()).getLastUsedAccountId();
   3736         final long id = Account.getDefaultAccountId(getContext(), lastUsedAccountId);
   3737         if (id == Account.NO_ACCOUNT) return;
   3738 
   3739         // Build a map of the requested columns to the appropriate positions
   3740         final ImmutableMap.Builder<String, Integer> builder =
   3741                 new ImmutableMap.Builder<String, Integer>();
   3742         final String[] columnNames = mc.getColumnNames();
   3743         for (int i = 0; i < columnNames.length; i++) {
   3744             builder.put(columnNames[i], i);
   3745         }
   3746         final Map<String, Integer> colPosMap = builder.build();
   3747 
   3748         final MailPrefs mailPrefs = MailPrefs.get(getContext());
   3749         final Object[] values = new Object[columnNames.length];
   3750         if (colPosMap.containsKey(BaseColumns._ID)) {
   3751             values[colPosMap.get(BaseColumns._ID)] = 0;
   3752         }
   3753         if (colPosMap.containsKey(UIProvider.AccountColumns.CAPABILITIES)) {
   3754             values[colPosMap.get(UIProvider.AccountColumns.CAPABILITIES)] =
   3755                     AccountCapabilities.UNDO |
   3756                     AccountCapabilities.VIRTUAL_ACCOUNT |
   3757                     AccountCapabilities.CLIENT_SANITIZED_HTML;
   3758         }
   3759         if (colPosMap.containsKey(UIProvider.AccountColumns.FOLDER_LIST_URI)) {
   3760             values[colPosMap.get(UIProvider.AccountColumns.FOLDER_LIST_URI)] =
   3761                     combinedUriString("uifolders", COMBINED_ACCOUNT_ID_STRING);
   3762         }
   3763         if (colPosMap.containsKey(UIProvider.AccountColumns.NAME)) {
   3764             values[colPosMap.get(UIProvider.AccountColumns.NAME)] = getContext().getString(
   3765                     R.string.mailbox_list_account_selector_combined_view);
   3766         }
   3767         if (colPosMap.containsKey(UIProvider.AccountColumns.ACCOUNT_MANAGER_NAME)) {
   3768             values[colPosMap.get(UIProvider.AccountColumns.ACCOUNT_MANAGER_NAME)] =
   3769                     getContext().getString(R.string.mailbox_list_account_selector_combined_view);
   3770         }
   3771         if (colPosMap.containsKey(UIProvider.AccountColumns.ACCOUNT_ID)) {
   3772             values[colPosMap.get(UIProvider.AccountColumns.ACCOUNT_ID)] = "Account Id";
   3773         }
   3774         if (colPosMap.containsKey(UIProvider.AccountColumns.TYPE)) {
   3775             values[colPosMap.get(UIProvider.AccountColumns.TYPE)] = "unknown";
   3776         }
   3777         if (colPosMap.containsKey(UIProvider.AccountColumns.UNDO_URI)) {
   3778             values[colPosMap.get(UIProvider.AccountColumns.UNDO_URI)] =
   3779                     "'content://" + EmailContent.AUTHORITY + "/uiundo'";
   3780         }
   3781         if (colPosMap.containsKey(UIProvider.AccountColumns.URI)) {
   3782             values[colPosMap.get(UIProvider.AccountColumns.URI)] =
   3783                     combinedUriString("uiaccount", COMBINED_ACCOUNT_ID_STRING);
   3784         }
   3785         if (colPosMap.containsKey(UIProvider.AccountColumns.MIME_TYPE)) {
   3786             values[colPosMap.get(UIProvider.AccountColumns.MIME_TYPE)] =
   3787                     EMAIL_APP_MIME_TYPE;
   3788         }
   3789         if (colPosMap.containsKey(UIProvider.AccountColumns.SECURITY_HOLD)) {
   3790             values[colPosMap.get(UIProvider.AccountColumns.SECURITY_HOLD)] = 0;
   3791         }
   3792         if (colPosMap.containsKey(UIProvider.AccountColumns.ACCOUNT_SECURITY_URI)) {
   3793             values[colPosMap.get(UIProvider.AccountColumns.ACCOUNT_SECURITY_URI)] = "";
   3794         }
   3795         if (colPosMap.containsKey(UIProvider.AccountColumns.SETTINGS_INTENT_URI)) {
   3796             values[colPosMap.get(UIProvider.AccountColumns.SETTINGS_INTENT_URI)] =
   3797                     getExternalUriString("settings", COMBINED_ACCOUNT_ID_STRING);
   3798         }
   3799         if (colPosMap.containsKey(UIProvider.AccountColumns.COMPOSE_URI)) {
   3800             values[colPosMap.get(UIProvider.AccountColumns.COMPOSE_URI)] =
   3801                     getExternalUriStringEmail2("compose", Long.toString(id));
   3802         }
   3803         if (colPosMap.containsKey(UIProvider.AccountColumns.UPDATE_SETTINGS_URI)) {
   3804             values[colPosMap.get(UIProvider.AccountColumns.UPDATE_SETTINGS_URI)] =
   3805                     uiUriString("uiacctsettings", -1);
   3806         }
   3807 
   3808         if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)) {
   3809             values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)] =
   3810                     Integer.toString(mailPrefs.getAutoAdvanceMode());
   3811         }
   3812         if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS)) {
   3813             values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS)] =
   3814                     Integer.toString(UIProvider.SnapHeaderValue.ALWAYS);
   3815         }
   3816         //.add(UIProvider.SettingsColumns.SIGNATURE, AccountColumns.SIGNATURE)
   3817         if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)) {
   3818             values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)] =
   3819                     Integer.toString(mailPrefs.getDefaultReplyAll()
   3820                             ? UIProvider.DefaultReplyBehavior.REPLY_ALL
   3821                             : UIProvider.DefaultReplyBehavior.REPLY);
   3822         }
   3823         if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)) {
   3824             values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)] =
   3825                     getConversationListIcon(mailPrefs);
   3826         }
   3827         if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) {
   3828             values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)] =
   3829                     mailPrefs.getConfirmDelete() ? 1 : 0;
   3830         }
   3831         if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)) {
   3832             values[colPosMap.get(
   3833                     UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)] = 0;
   3834         }
   3835         if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) {
   3836             values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)] =
   3837                     mailPrefs.getConfirmSend() ? 1 : 0;
   3838         }
   3839         if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)) {
   3840             values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)] =
   3841                     combinedUriString("uifolder", combinedMailboxId(Mailbox.TYPE_INBOX));
   3842         }
   3843         if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX)) {
   3844             values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX)] =
   3845                     combinedUriString("uifolder", combinedMailboxId(Mailbox.TYPE_INBOX));
   3846         }
   3847         if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.SHOW_IMAGES)) {
   3848             values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.SHOW_IMAGES)] =
   3849                     Settings.ShowImages.ASK_FIRST;
   3850         }
   3851 
   3852         mc.addRow(values);
   3853     }
   3854 
   3855     private static int getConversationListIcon(MailPrefs mailPrefs) {
   3856         return mailPrefs.getShowSenderImages() ?
   3857                 UIProvider.ConversationListIcon.SENDER_IMAGE :
   3858                 UIProvider.ConversationListIcon.NONE;
   3859     }
   3860 
   3861     private Cursor getVirtualMailboxCursor(long mailboxId, String[] projection) {
   3862         MatrixCursor mc = new MatrixCursorWithCachedColumns(projection, 1);
   3863         mc.addRow(getVirtualMailboxRow(getVirtualMailboxAccountId(mailboxId),
   3864                 getVirtualMailboxType(mailboxId), projection));
   3865         return mc;
   3866     }
   3867 
   3868     private Object[] getVirtualMailboxRow(long accountId, int mailboxType, String[] projection) {
   3869         final long id = getVirtualMailboxId(accountId, mailboxType);
   3870         final String idString = Long.toString(id);
   3871         Object[] values = new Object[projection.length];
   3872         // Not all column values are filled in here, as some are not applicable to virtual mailboxes
   3873         // The remainder are left null
   3874         for (int i = 0; i < projection.length; i++) {
   3875             final String column = projection[i];
   3876             if (column.equals(UIProvider.FolderColumns._ID)) {
   3877                 values[i] = id;
   3878             } else if (column.equals(UIProvider.FolderColumns.URI)) {
   3879                 values[i] = combinedUriString("uifolder", idString);
   3880             } else if (column.equals(UIProvider.FolderColumns.NAME)) {
   3881                 // default empty string since all of these should use resource strings
   3882                 values[i] = getFolderDisplayName(getFolderTypeFromMailboxType(mailboxType), "");
   3883             } else if (column.equals(UIProvider.FolderColumns.HAS_CHILDREN)) {
   3884                 values[i] = 0;
   3885             } else if (column.equals(UIProvider.FolderColumns.CAPABILITIES)) {
   3886                 values[i] = UIProvider.FolderCapabilities.DELETE
   3887                         | UIProvider.FolderCapabilities.IS_VIRTUAL;
   3888             } else if (column.equals(UIProvider.FolderColumns.CONVERSATION_LIST_URI)) {
   3889                 values[i] = combinedUriString("uimessages", idString);
   3890             } else if (column.equals(UIProvider.FolderColumns.UNREAD_COUNT)) {
   3891                 if (mailboxType == Mailbox.TYPE_INBOX && accountId == COMBINED_ACCOUNT_ID) {
   3892                     final int unreadCount = EmailContent.count(getContext(), Message.CONTENT_URI,
   3893                             MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns._ID
   3894                             + " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE
   3895                             + "=" + Mailbox.TYPE_INBOX + ") AND " + MessageColumns.FLAG_READ + "=0",
   3896                             null);
   3897                     values[i] = unreadCount;
   3898                 } else if (mailboxType == Mailbox.TYPE_UNREAD) {
   3899                     final String accountKeyClause;
   3900                     final String[] whereArgs;
   3901                     if (accountId == COMBINED_ACCOUNT_ID) {
   3902                         accountKeyClause = "";
   3903                         whereArgs = null;
   3904                     } else {
   3905                         accountKeyClause = MessageColumns.ACCOUNT_KEY + "= ? AND ";
   3906                         whereArgs = new String[] { Long.toString(accountId) };
   3907                     }
   3908                     final int unreadCount = EmailContent.count(getContext(), Message.CONTENT_URI,
   3909                             accountKeyClause + MessageColumns.FLAG_READ + "=0 AND "
   3910                             + MessageColumns.MAILBOX_KEY + " NOT IN (SELECT " + MailboxColumns._ID
   3911                             + " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE + "="
   3912                             + Mailbox.TYPE_TRASH + ")", whereArgs);
   3913                     values[i] = unreadCount;
   3914                 } else if (mailboxType == Mailbox.TYPE_STARRED) {
   3915                     final String accountKeyClause;
   3916                     final String[] whereArgs;
   3917                     if (accountId == COMBINED_ACCOUNT_ID) {
   3918                         accountKeyClause = "";
   3919                         whereArgs = null;
   3920                     } else {
   3921                         accountKeyClause = MessageColumns.ACCOUNT_KEY + "= ? AND ";
   3922                         whereArgs = new String[] { Long.toString(accountId) };
   3923                     }
   3924                     final int starredCount = EmailContent.count(getContext(), Message.CONTENT_URI,
   3925                             accountKeyClause + MessageColumns.FLAG_FAVORITE + "=1", whereArgs);
   3926                     values[i] = starredCount;
   3927                 }
   3928             } else if (column.equals(UIProvider.FolderColumns.ICON_RES_ID)) {
   3929                 if (mailboxType == Mailbox.TYPE_INBOX) {
   3930                     values[i] = R.drawable.ic_drawer_inbox_24dp;
   3931                 } else if (mailboxType == Mailbox.TYPE_UNREAD) {
   3932                     values[i] = R.drawable.ic_drawer_unread_24dp;
   3933                 } else if (mailboxType == Mailbox.TYPE_STARRED) {
   3934                     values[i] = R.drawable.ic_drawer_starred_24dp;
   3935                 }
   3936             }
   3937         }
   3938         return values;
   3939     }
   3940 
   3941     private Cursor uiAccounts(String[] uiProjection, boolean suppressCombined) {
   3942         final Context context = getContext();
   3943         final SQLiteDatabase db = getDatabase(context);
   3944         final Cursor accountIdCursor =
   3945                 db.rawQuery("select _id from " + Account.TABLE_NAME, new String[0]);
   3946         final MatrixCursor mc;
   3947         try {
   3948             boolean combinedAccount = false;
   3949             if (!suppressCombined && accountIdCursor.getCount() > 1) {
   3950                 combinedAccount = true;
   3951             }
   3952             final Bundle extras = new Bundle();
   3953             // Email always returns the accurate number of accounts
   3954             extras.putInt(AccountCursorExtraKeys.ACCOUNTS_LOADED, 1);
   3955             mc = new MatrixCursorWithExtra(uiProjection, accountIdCursor.getCount(), extras);
   3956             final Object[] values = new Object[uiProjection.length];
   3957             while (accountIdCursor.moveToNext()) {
   3958                 final String id = accountIdCursor.getString(0);
   3959                 final Cursor accountCursor =
   3960                         db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id});
   3961                 try {
   3962                     if (accountCursor.moveToNext()) {
   3963                         for (int i = 0; i < uiProjection.length; i++) {
   3964                             values[i] = accountCursor.getString(i);
   3965                         }
   3966                         mc.addRow(values);
   3967                     }
   3968                 } finally {
   3969                     accountCursor.close();
   3970                 }
   3971             }
   3972             if (combinedAccount) {
   3973                 addCombinedAccountRow(mc);
   3974             }
   3975         } finally {
   3976             accountIdCursor.close();
   3977         }
   3978         mc.setNotificationUri(context.getContentResolver(), UIPROVIDER_ALL_ACCOUNTS_NOTIFIER);
   3979 
   3980         return mc;
   3981     }
   3982 
   3983     private Cursor uiQuickResponseAccount(String[] uiProjection, String account) {
   3984         final Context context = getContext();
   3985         final SQLiteDatabase db = getDatabase(context);
   3986         final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection);
   3987         sb.append(" FROM " + QuickResponse.TABLE_NAME);
   3988         sb.append(" WHERE " + QuickResponse.ACCOUNT_KEY + "=?");
   3989         final String query = sb.toString();
   3990         return db.rawQuery(query, new String[] {account});
   3991     }
   3992 
   3993     private Cursor uiQuickResponseId(String[] uiProjection, String id) {
   3994         final Context context = getContext();
   3995         final SQLiteDatabase db = getDatabase(context);
   3996         final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection);
   3997         sb.append(" FROM " + QuickResponse.TABLE_NAME);
   3998         sb.append(" WHERE " + QuickResponse._ID + "=?");
   3999         final String query = sb.toString();
   4000         return db.rawQuery(query, new String[] {id});
   4001     }
   4002 
   4003     private Cursor uiQuickResponse(String[] uiProjection) {
   4004         final Context context = getContext();
   4005         final SQLiteDatabase db = getDatabase(context);
   4006         final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection);
   4007         sb.append(" FROM " + QuickResponse.TABLE_NAME);
   4008         final String query = sb.toString();
   4009         return db.rawQuery(query, new String[0]);
   4010     }
   4011 
   4012     /**
   4013      * Generate the "attachment list" SQLite query, given a projection from UnifiedEmail
   4014      *
   4015      * @param uiProjection as passed from UnifiedEmail
   4016      * @param contentTypeQueryParameters list of mimeTypes, used as a filter for the attachments
   4017      * or null if there are no query parameters
   4018      * @return the SQLite query to be executed on the EmailProvider database
   4019      */
   4020     private static String genQueryAttachments(String[] uiProjection,
   4021             List<String> contentTypeQueryParameters) {
   4022         // MAKE SURE THESE VALUES STAY IN SYNC WITH GEN QUERY ATTACHMENT
   4023         ContentValues values = new ContentValues(1);
   4024         values.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, 1);
   4025         StringBuilder sb = genSelect(getAttachmentMap(), uiProjection, values);
   4026         sb.append(" FROM ")
   4027                 .append(Attachment.TABLE_NAME)
   4028                 .append(" WHERE ")
   4029                 .append(AttachmentColumns.MESSAGE_KEY)
   4030                 .append(" =? ");
   4031 
   4032         // Filter for certain content types.
   4033         // The filter works by adding LIKE operators for each
   4034         // content type you wish to request. Content types
   4035         // are filtered by performing a case-insensitive "starts with"
   4036         // filter. IE, "image/" would return "image/png" as well as "image/jpeg".
   4037         if (contentTypeQueryParameters != null && !contentTypeQueryParameters.isEmpty()) {
   4038             final int size = contentTypeQueryParameters.size();
   4039             sb.append("AND (");
   4040             for (int i = 0; i < size; i++) {
   4041                 final String contentType = contentTypeQueryParameters.get(i);
   4042                 sb.append(AttachmentColumns.MIME_TYPE)
   4043                         .append(" LIKE '")
   4044                         .append(contentType)
   4045                         .append("%'");
   4046 
   4047                 if (i != size - 1) {
   4048                     sb.append(" OR ");
   4049                 }
   4050             }
   4051             sb.append(")");
   4052         }
   4053         return sb.toString();
   4054     }
   4055 
   4056     /**
   4057      * Generate the "single attachment" SQLite query, given a projection from UnifiedEmail
   4058      *
   4059      * @param uiProjection as passed from UnifiedEmail
   4060      * @return the SQLite query to be executed on the EmailProvider database
   4061      */
   4062     private String genQueryAttachment(String[] uiProjection) {
   4063         // MAKE SURE THESE VALUES STAY IN SYNC WITH GEN QUERY ATTACHMENTS
   4064         final ContentValues values = new ContentValues(2);
   4065         values.put(AttachmentColumns.CONTENT_URI, createAttachmentUriColumnSQL());
   4066         values.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, 1);
   4067 
   4068         return genSelect(getAttachmentMap(), uiProjection, values)
   4069                 .append(" FROM ").append(Attachment.TABLE_NAME)
   4070                 .append(" WHERE ")
   4071                 .append(AttachmentColumns._ID).append(" =? ")
   4072                 .toString();
   4073     }
   4074 
   4075     /**
   4076      * Generate the "single attachment by Content ID" SQLite query, given a projection from
   4077      * UnifiedEmail
   4078      *
   4079      * @param uiProjection as passed from UnifiedEmail
   4080      * @return the SQLite query to be executed on the EmailProvider database
   4081      */
   4082     private String genQueryAttachmentByMessageIDAndCid(String[] uiProjection) {
   4083         final ContentValues values = new ContentValues(2);
   4084         values.put(AttachmentColumns.CONTENT_URI, createAttachmentUriColumnSQL());
   4085         values.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, 1);
   4086 
   4087         return genSelect(getAttachmentMap(), uiProjection, values)
   4088                 .append(" FROM ").append(Attachment.TABLE_NAME)
   4089                 .append(" WHERE ")
   4090                 .append(AttachmentColumns.MESSAGE_KEY).append(" =? ")
   4091                 .append(" AND ")
   4092                 .append(AttachmentColumns.CONTENT_ID).append(" =? ")
   4093                 .toString();
   4094     }
   4095 
   4096     /**
   4097      * @return a fragment of SQL that is the expression which, when evaluated for a particular
   4098      *      Attachment row, produces the Content URI for the attachment
   4099      */
   4100     private static String createAttachmentUriColumnSQL() {
   4101         final String uriPrefix = Attachment.ATTACHMENT_PROVIDER_URI_PREFIX;
   4102         final String accountKey = AttachmentColumns.ACCOUNT_KEY;
   4103         final String id = AttachmentColumns._ID;
   4104         final String raw = AttachmentUtilities.FORMAT_RAW;
   4105         final String contentUri = String.format("%s/' || %s || '/' || %s || '/%s", uriPrefix,
   4106                 accountKey, id, raw);
   4107 
   4108         return "@CASE " +
   4109                 "WHEN contentUri IS NULL THEN '" + contentUri + "' " +
   4110                 "WHEN contentUri IS NOT NULL THEN contentUri " +
   4111                 "END";
   4112     }
   4113 
   4114     /**
   4115      * Generate the "subfolder list" SQLite query, given a projection from UnifiedEmail
   4116      *
   4117      * @param uiProjection as passed from UnifiedEmail
   4118      * @return the SQLite query to be executed on the EmailProvider database
   4119      */
   4120     private static String genQuerySubfolders(String[] uiProjection) {
   4121         StringBuilder sb = genSelect(getFolderListMap(), uiProjection);
   4122         sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.PARENT_KEY +
   4123                 " =? ORDER BY ");
   4124         sb.append(MAILBOX_ORDER_BY);
   4125         return sb.toString();
   4126     }
   4127 
   4128     private static final String COMBINED_ACCOUNT_ID_STRING = Long.toString(COMBINED_ACCOUNT_ID);
   4129 
   4130     /**
   4131      * Returns a cursor over all the folders for a specific URI which corresponds to a single
   4132      * account.
   4133      * @param uri uri to query
   4134      * @param uiProjection projection
   4135      * @return query result cursor
   4136      */
   4137     private Cursor uiFolders(final Uri uri, final String[] uiProjection) {
   4138         final Context context = getContext();
   4139         final SQLiteDatabase db = getDatabase(context);
   4140         final String id = uri.getPathSegments().get(1);
   4141 
   4142         final Uri notifyUri =
   4143                 UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build();
   4144 
   4145         final Cursor vc = uiVirtualMailboxes(id, uiProjection);
   4146         vc.setNotificationUri(context.getContentResolver(), notifyUri);
   4147         if (id.equals(COMBINED_ACCOUNT_ID_STRING)) {
   4148             return vc;
   4149         } else {
   4150             Cursor c = db.rawQuery(genQueryAccountMailboxes(UIProvider.FOLDERS_PROJECTION),
   4151                     new String[] {id});
   4152             c = getFolderListCursor(c, Long.valueOf(id), uiProjection);
   4153             c.setNotificationUri(context.getContentResolver(), notifyUri);
   4154             if (c.getCount() > 0) {
   4155                 Cursor[] cursors = new Cursor[]{vc, c};
   4156                 return new MergeCursor(cursors);
   4157             } else {
   4158                 return c;
   4159             }
   4160         }
   4161     }
   4162 
   4163     private Cursor uiVirtualMailboxes(final String id, final String[] uiProjection) {
   4164         final MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection);
   4165 
   4166         if (id.equals(COMBINED_ACCOUNT_ID_STRING)) {
   4167             mc.addRow(getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_INBOX, uiProjection));
   4168             mc.addRow(
   4169                     getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_STARRED, uiProjection));
   4170             mc.addRow(getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_UNREAD, uiProjection));
   4171         } else {
   4172             final long acctId = Long.parseLong(id);
   4173             mc.addRow(getVirtualMailboxRow(acctId, Mailbox.TYPE_STARRED, uiProjection));
   4174             mc.addRow(getVirtualMailboxRow(acctId, Mailbox.TYPE_UNREAD, uiProjection));
   4175         }
   4176 
   4177         return mc;
   4178     }
   4179 
   4180     /**
   4181      * Returns an array of the default recent folders for a given URI which is unique for an
   4182      * account. Some accounts might not have default recent folders, in which case an empty array
   4183      * is returned.
   4184      * @param id account id
   4185      * @return array of URIs
   4186      */
   4187     private Uri[] defaultRecentFolders(final String id) {
   4188         Uri[] recentFolders = new Uri[0];
   4189         final SQLiteDatabase db = getDatabase(getContext());
   4190         if (id.equals(COMBINED_ACCOUNT_ID_STRING)) {
   4191             // We don't have default recents for the combined view.
   4192             return recentFolders;
   4193         }
   4194         // We search for the types we want, and find corresponding IDs.
   4195         final String[] idAndType = { BaseColumns._ID, UIProvider.FolderColumns.TYPE };
   4196 
   4197         // Sent, Drafts, and Starred are the default recents.
   4198         final StringBuilder sb = genSelect(getFolderListMap(), idAndType);
   4199         sb.append(" FROM ")
   4200                 .append(Mailbox.TABLE_NAME)
   4201                 .append(" WHERE ")
   4202                 .append(MailboxColumns.ACCOUNT_KEY)
   4203                 .append(" = ")
   4204                 .append(id)
   4205                 .append(" AND ")
   4206                 .append(MailboxColumns.TYPE)
   4207                 .append(" IN (")
   4208                 .append(Mailbox.TYPE_SENT)
   4209                 .append(", ")
   4210                 .append(Mailbox.TYPE_DRAFTS)
   4211                 .append(", ")
   4212                 .append(Mailbox.TYPE_STARRED)
   4213                 .append(")");
   4214         LogUtils.d(TAG, "defaultRecentFolders: Query is %s", sb);
   4215         final Cursor c = db.rawQuery(sb.toString(), null);
   4216         try {
   4217             if (c == null || c.getCount() <= 0 || !c.moveToFirst()) {
   4218                 return recentFolders;
   4219             }
   4220             // Read all the IDs of the mailboxes, and turn them into URIs.
   4221             recentFolders = new Uri[c.getCount()];
   4222             int i = 0;
   4223             do {
   4224                 final long folderId = c.getLong(0);
   4225                 recentFolders[i] = uiUri("uifolder", folderId);
   4226                 LogUtils.d(TAG, "Default recent folder: %d, with uri %s", folderId,
   4227                         recentFolders[i]);
   4228                 ++i;
   4229             } while (c.moveToNext());
   4230         } finally {
   4231             if (c != null) {
   4232                 c.close();
   4233             }
   4234         }
   4235         return recentFolders;
   4236     }
   4237 
   4238     /**
   4239      * Convenience method to create a {@link Folder}
   4240      * @param context to get a {@link ContentResolver}
   4241      * @param mailboxId id of the {@link Mailbox} that we want
   4242      * @return the {@link Folder} or null
   4243      */
   4244     public static Folder getFolder(Context context, long mailboxId) {
   4245         final ContentResolver resolver = context.getContentResolver();
   4246         final Cursor fc = resolver.query(EmailProvider.uiUri("uifolder", mailboxId),
   4247                 UIProvider.FOLDERS_PROJECTION, null, null, null);
   4248 
   4249         if (fc == null) {
   4250             LogUtils.e(TAG, "Null folder cursor for mailboxId %d", mailboxId);
   4251             return null;
   4252         }
   4253 
   4254         Folder uiFolder = null;
   4255         try {
   4256             if (fc.moveToFirst()) {
   4257                 uiFolder = new Folder(fc);
   4258             }
   4259         } finally {
   4260             fc.close();
   4261         }
   4262         return uiFolder;
   4263     }
   4264 
   4265     static class AttachmentsCursor extends CursorWrapper {
   4266         private final int mContentUriIndex;
   4267         private final int mUriIndex;
   4268         private final Context mContext;
   4269         private final String[] mContentUriStrings;
   4270 
   4271         public AttachmentsCursor(Context context, Cursor cursor) {
   4272             super(cursor);
   4273             mContentUriIndex = cursor.getColumnIndex(UIProvider.AttachmentColumns.CONTENT_URI);
   4274             mUriIndex = cursor.getColumnIndex(UIProvider.AttachmentColumns.URI);
   4275             mContext = context;
   4276             mContentUriStrings = new String[cursor.getCount()];
   4277             if (mContentUriIndex == -1) {
   4278                 // Nothing to do here, move along
   4279                 return;
   4280             }
   4281             while (cursor.moveToNext()) {
   4282                 final int index = cursor.getPosition();
   4283                 final Uri uri = Uri.parse(getString(mUriIndex));
   4284                 final long id = Long.parseLong(uri.getLastPathSegment());
   4285                 final Attachment att = Attachment.restoreAttachmentWithId(mContext, id);
   4286 
   4287                 if (att == null) {
   4288                     mContentUriStrings[index] = "";
   4289                     continue;
   4290                 }
   4291 
   4292                 if (!TextUtils.isEmpty(att.getCachedFileUri())) {
   4293                     mContentUriStrings[index] = att.getCachedFileUri();
   4294                     continue;
   4295                 }
   4296 
   4297                 final String contentUri;
   4298                 // Until the package installer can handle opening apks from a content:// uri, for
   4299                 // any apk that was successfully saved in external storage, return the
   4300                 // content uri from the attachment
   4301                 if (att.mUiDestination == UIProvider.AttachmentDestination.EXTERNAL &&
   4302                         att.mUiState == UIProvider.AttachmentState.SAVED &&
   4303                         TextUtils.equals(att.mMimeType, MimeType.ANDROID_ARCHIVE)) {
   4304                     contentUri = att.getContentUri();
   4305                 } else {
   4306                     final String attUriString = att.getContentUri();
   4307                     final String authority;
   4308                     if (!TextUtils.isEmpty(attUriString)) {
   4309                         authority = Uri.parse(attUriString).getAuthority();
   4310                     } else {
   4311                         authority = null;
   4312                     }
   4313                     if (TextUtils.equals(authority, Attachment.ATTACHMENT_PROVIDER_AUTHORITY)) {
   4314                         contentUri = attUriString;
   4315                     } else {
   4316                         contentUri = AttachmentUtilities.getAttachmentUri(att.mAccountKey, id)
   4317                                 .toString();
   4318                     }
   4319                 }
   4320                 mContentUriStrings[index] = contentUri;
   4321 
   4322             }
   4323             cursor.moveToPosition(-1);
   4324         }
   4325 
   4326         @Override
   4327         public String getString(int column) {
   4328             if (column == mContentUriIndex) {
   4329                 return mContentUriStrings[getPosition()];
   4330             } else {
   4331                 return super.getString(column);
   4332             }
   4333         }
   4334     }
   4335 
   4336     /**
   4337      * For debugging purposes; shouldn't be used in production code
   4338      */
   4339     @SuppressWarnings("unused")
   4340     static class CloseDetectingCursor extends CursorWrapper {
   4341 
   4342         public CloseDetectingCursor(Cursor cursor) {
   4343             super(cursor);
   4344         }
   4345 
   4346         @Override
   4347         public void close() {
   4348             super.close();
   4349             LogUtils.d(TAG, "Closing cursor", new Error());
   4350         }
   4351     }
   4352 
   4353     /**
   4354      * Converts a mailbox in a row of the mailboxCursor into a row
   4355      * in the supplied {@link MatrixCursor} in the format required for {@link Folder}.
   4356      * As a convenience, the modified {@link MatrixCursor} is also returned.
   4357      * @param mc the {@link MatrixCursor} into which the mailbox data will be converted
   4358      * @param projectionLength the length of the projection for this Cursor
   4359      * @param mailboxCursor the cursor supplying the mailbox data
   4360      * @param nameColumn column in the cursor containing the folder name value
   4361      * @param typeColumn column in the cursor containing the folder type value
   4362      * @return the {@link MatrixCursor} containing the transformed data.
   4363      */
   4364     private Cursor getUiFolderCursorRowFromMailboxCursorRow(
   4365             MatrixCursor mc, int projectionLength, Cursor mailboxCursor,
   4366             int nameColumn, int typeColumn) {
   4367         final MatrixCursor.RowBuilder builder = mc.newRow();
   4368         for (int i = 0; i < projectionLength; i++) {
   4369             // If we are at the name column, get the type
   4370             // and use it to use a properly translated string
   4371             // from resources instead of the display name.
   4372             // This ignores display names for system mailboxes.
   4373             if (nameColumn == i) {
   4374                 // We implicitly assume that if name is requested,
   4375                 // type has also been requested. If not, this will
   4376                 // error in unknown ways.
   4377                 final int type = mailboxCursor.getInt(typeColumn);
   4378                 builder.add(getFolderDisplayName(type, mailboxCursor.getString(i)));
   4379             } else {
   4380                 builder.add(mailboxCursor.getString(i));
   4381             }
   4382         }
   4383         return mc;
   4384     }
   4385 
   4386     /**
   4387      * Takes a uifolder cursor (that was generated with a full projection) and remaps values for
   4388      * columns that are difficult to generate in the SQL query. This currently includes:
   4389      * - Folder name (due to system folder localization).
   4390      * - Capabilities (due to this varying by account protocol).
   4391      * - Persistent id (due to needing to base64 encode it).
   4392      * - Load more uri (due to this varying by account protocol).
   4393      * TODO: This would be better as a CursorWrapper, rather than doing a copy.
   4394      * @param inputCursor A cursor containing all columns of {@link UIProvider.FolderColumns}.
   4395      *                    Strictly speaking doesn't need all, but simpler if we assume that.
   4396      * @param outputCursor A MatrixCursor which this function will populate.
   4397      * @param accountId The account id for the mailboxes in this query.
   4398      * @param uiProjection The projection specified by the query.
   4399      */
   4400     private void remapFolderCursor(final Cursor inputCursor, final MatrixCursor outputCursor,
   4401             final long accountId, final String[] uiProjection) {
   4402         // Return early if our input cursor is empty.
   4403         if (inputCursor == null || inputCursor.getCount() == 0) {
   4404             return;
   4405         }
   4406         // Get the column indices for the columns we need during remapping.
   4407         // While we currently could assume the column indices for UIProvider.FOLDERS_PROJECTION
   4408         // and therefore avoid the calls to getColumnIndex, this at least tries to future-proof a
   4409         // bit.
   4410         // Note that id and type MUST be present for this function to work correctly.
   4411         final int idColumn = inputCursor.getColumnIndex(BaseColumns._ID);
   4412         final int typeColumn = inputCursor.getColumnIndex(UIProvider.FolderColumns.TYPE);
   4413         final int nameColumn = inputCursor.getColumnIndex(UIProvider.FolderColumns.NAME);
   4414         final int capabilitiesColumn =
   4415                 inputCursor.getColumnIndex(UIProvider.FolderColumns.CAPABILITIES);
   4416         final int persistentIdColumn =
   4417                 inputCursor.getColumnIndex(UIProvider.FolderColumns.PERSISTENT_ID);
   4418         final int loadMoreUriColumn =
   4419                 inputCursor.getColumnIndex(UIProvider.FolderColumns.LOAD_MORE_URI);
   4420 
   4421         // Get the EmailServiceInfo for the current account.
   4422         final Context context = getContext();
   4423         final String protocol = Account.getProtocol(context, accountId);
   4424         final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol);
   4425 
   4426         // Build the return cursor. We iterate over all rows of the input cursor and construct
   4427         // a row in the output using the columns in uiProjection.
   4428         while (inputCursor.moveToNext()) {
   4429             final MatrixCursor.RowBuilder builder = outputCursor.newRow();
   4430             final int folderType = inputCursor.getInt(typeColumn);
   4431             for (int i = 0; i < uiProjection.length; i++) {
   4432                 // Find the index in the input cursor corresponding the column requested in the
   4433                 // output projection.
   4434                 final int index = inputCursor.getColumnIndex(uiProjection[i]);
   4435                 if (index == -1) {
   4436                     // We don't have this value, so put a blank in the output and move on.
   4437                     builder.add(null);
   4438                     continue;
   4439                 }
   4440                 final String value = inputCursor.getString(index);
   4441                 // remapped indicates whether we've written a value to the output for this column.
   4442                 final boolean remapped;
   4443                 if (nameColumn == index) {
   4444                     // Remap folder name for system folders.
   4445                     builder.add(getFolderDisplayName(folderType, value));
   4446                     remapped = true;
   4447                 } else if (capabilitiesColumn == index) {
   4448                     // Get the correct capabilities for this folder.
   4449                     final long mailboxID = inputCursor.getLong(idColumn);
   4450                     final int mailboxType = getMailboxTypeFromFolderType(folderType);
   4451                     builder.add(getFolderCapabilities(info, mailboxType, mailboxID));
   4452                     remapped = true;
   4453                 } else if (persistentIdColumn == index) {
   4454                     // Hash the persistent id.
   4455                     builder.add(Base64.encodeToString(value.getBytes(),
   4456                             Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING));
   4457                     remapped = true;
   4458                 } else if (loadMoreUriColumn == index && folderType != Mailbox.TYPE_SEARCH &&
   4459                         (info == null || !info.offerLoadMore)) {
   4460                     // Blank the load more uri for account types that don't offer it.
   4461                     // Note that all account types permit load more for search results.
   4462                     builder.add(null);
   4463                     remapped = true;
   4464                 } else {
   4465                     remapped = false;
   4466                 }
   4467                 // If the above logic didn't write some other value to the output, use the value
   4468                 // from the input cursor.
   4469                 if (!remapped) {
   4470                     builder.add(value);
   4471                 }
   4472             }
   4473         }
   4474     }
   4475 
   4476     private Cursor getFolderListCursor(final Cursor inputCursor, final long accountId,
   4477             final String[] uiProjection) {
   4478         final MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection);
   4479         if (inputCursor != null) {
   4480             try {
   4481                 remapFolderCursor(inputCursor, mc, accountId, uiProjection);
   4482             } finally {
   4483                 inputCursor.close();
   4484             }
   4485         }
   4486         return mc;
   4487     }
   4488 
   4489     /**
   4490      * Returns a {@link String} from Resources corresponding
   4491      * to the {@link UIProvider.FolderType} requested.
   4492      * @param folderType {@link UIProvider.FolderType} value for the folder
   4493      * @param defaultName a {@link String} to use in case the {@link UIProvider.FolderType}
   4494      *                    provided is not a system folder.
   4495      * @return a {@link String} to use as the display name for the folder
   4496      */
   4497     private String getFolderDisplayName(int folderType, String defaultName) {
   4498         final int resId;
   4499         switch (folderType) {
   4500             case UIProvider.FolderType.INBOX:
   4501                 resId = R.string.mailbox_name_display_inbox;
   4502                 break;
   4503             case UIProvider.FolderType.OUTBOX:
   4504                 resId = R.string.mailbox_name_display_outbox;
   4505                 break;
   4506             case UIProvider.FolderType.DRAFT:
   4507                 resId = R.string.mailbox_name_display_drafts;
   4508                 break;
   4509             case UIProvider.FolderType.TRASH:
   4510                 resId = R.string.mailbox_name_display_trash;
   4511                 break;
   4512             case UIProvider.FolderType.SENT:
   4513                 resId = R.string.mailbox_name_display_sent;
   4514                 break;
   4515             case UIProvider.FolderType.SPAM:
   4516                 resId = R.string.mailbox_name_display_junk;
   4517                 break;
   4518             case UIProvider.FolderType.STARRED:
   4519                 resId = R.string.mailbox_name_display_starred;
   4520                 break;
   4521             case UIProvider.FolderType.UNREAD:
   4522                 resId = R.string.mailbox_name_display_unread;
   4523                 break;
   4524             default:
   4525                 return defaultName;
   4526         }
   4527         return getContext().getString(resId);
   4528     }
   4529 
   4530     /**
   4531      * Converts a {@link Mailbox} type value to its {@link UIProvider.FolderType}
   4532      * equivalent.
   4533      * @param mailboxType a {@link Mailbox} type
   4534      * @return a {@link UIProvider.FolderType} value
   4535      */
   4536     private static int getFolderTypeFromMailboxType(int mailboxType) {
   4537         switch (mailboxType) {
   4538             case Mailbox.TYPE_INBOX:
   4539                 return UIProvider.FolderType.INBOX;
   4540             case Mailbox.TYPE_OUTBOX:
   4541                 return UIProvider.FolderType.OUTBOX;
   4542             case Mailbox.TYPE_DRAFTS:
   4543                 return UIProvider.FolderType.DRAFT;
   4544             case Mailbox.TYPE_TRASH:
   4545                 return UIProvider.FolderType.TRASH;
   4546             case Mailbox.TYPE_SENT:
   4547                 return UIProvider.FolderType.SENT;
   4548             case Mailbox.TYPE_JUNK:
   4549                 return UIProvider.FolderType.SPAM;
   4550             case Mailbox.TYPE_STARRED:
   4551                 return UIProvider.FolderType.STARRED;
   4552             case Mailbox.TYPE_UNREAD:
   4553                 return UIProvider.FolderType.UNREAD;
   4554             case Mailbox.TYPE_SEARCH:
   4555                 // TODO Can the DEFAULT type be removed from SEARCH folders?
   4556                 return UIProvider.FolderType.DEFAULT | UIProvider.FolderType.SEARCH;
   4557             default:
   4558                 return UIProvider.FolderType.DEFAULT;
   4559         }
   4560     }
   4561 
   4562     /**
   4563      * Converts a {@link UIProvider.FolderType} type value to its {@link Mailbox} equivalent.
   4564      * @param folderType a {@link UIProvider.FolderType} type
   4565      * @return a {@link Mailbox} value
   4566      */
   4567     private static int getMailboxTypeFromFolderType(int folderType) {
   4568         switch (folderType) {
   4569             case UIProvider.FolderType.DEFAULT:
   4570                 return Mailbox.TYPE_MAIL;
   4571             case UIProvider.FolderType.INBOX:
   4572                 return Mailbox.TYPE_INBOX;
   4573             case UIProvider.FolderType.OUTBOX:
   4574                 return Mailbox.TYPE_OUTBOX;
   4575             case UIProvider.FolderType.DRAFT:
   4576                 return Mailbox.TYPE_DRAFTS;
   4577             case UIProvider.FolderType.TRASH:
   4578                 return Mailbox.TYPE_TRASH;
   4579             case UIProvider.FolderType.SENT:
   4580                 return Mailbox.TYPE_SENT;
   4581             case UIProvider.FolderType.SPAM:
   4582                 return Mailbox.TYPE_JUNK;
   4583             case UIProvider.FolderType.STARRED:
   4584                 return Mailbox.TYPE_STARRED;
   4585             case UIProvider.FolderType.UNREAD:
   4586                 return Mailbox.TYPE_UNREAD;
   4587             case UIProvider.FolderType.DEFAULT | UIProvider.FolderType.SEARCH:
   4588                 // TODO Can the DEFAULT type be removed from SEARCH folders?
   4589                 return Mailbox.TYPE_SEARCH;
   4590             default:
   4591                 throw new IllegalArgumentException("Unable to map folder type: " + folderType);
   4592         }
   4593     }
   4594 
   4595     /**
   4596      * We need a reasonably full projection for getFolderListCursor to work, but don't always want
   4597      * to do the subquery needed for FolderColumns.UNREAD_SENDERS
   4598      * @param uiProjection The projection we actually want
   4599      * @return Full projection, possibly with or without FolderColumns.UNREAD_SENDERS
   4600      */
   4601     private String[] folderProjectionFromUiProjection(final String[] uiProjection) {
   4602         final Set<String> columns = ImmutableSet.copyOf(uiProjection);
   4603         if (columns.contains(UIProvider.FolderColumns.UNREAD_SENDERS)) {
   4604             return UIProvider.FOLDERS_PROJECTION_WITH_UNREAD_SENDERS;
   4605         } else {
   4606             return UIProvider.FOLDERS_PROJECTION;
   4607         }
   4608     }
   4609 
   4610     /**
   4611      * Handle UnifiedEmail queries here (dispatched from query())
   4612      *
   4613      * @param match the UriMatcher match for the original uri passed in from UnifiedEmail
   4614      * @param uri the original uri passed in from UnifiedEmail
   4615      * @param uiProjection the projection passed in from UnifiedEmail
   4616      * @param unseenOnly <code>true</code> to only return unseen messages (where supported)
   4617      * @return the result Cursor
   4618      */
   4619     private Cursor uiQuery(int match, Uri uri, String[] uiProjection, final boolean unseenOnly) {
   4620         Context context = getContext();
   4621         ContentResolver resolver = context.getContentResolver();
   4622         SQLiteDatabase db = getDatabase(context);
   4623         // Should we ever return null, or throw an exception??
   4624         Cursor c = null;
   4625         String id = uri.getPathSegments().get(1);
   4626         Uri notifyUri = null;
   4627         switch(match) {
   4628             case UI_ALL_FOLDERS:
   4629                 notifyUri =
   4630                         UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build();
   4631                 final Cursor vc = uiVirtualMailboxes(id, uiProjection);
   4632                 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) {
   4633                     // There's no real mailboxes, so just return the virtual ones
   4634                     c = vc;
   4635                 } else {
   4636                     // Return real and virtual mailboxes alike
   4637                     final Cursor rawc = db.rawQuery(genQueryAccountAllMailboxes(uiProjection),
   4638                             new String[] {id});
   4639                     rawc.setNotificationUri(context.getContentResolver(), notifyUri);
   4640                     vc.setNotificationUri(context.getContentResolver(), notifyUri);
   4641                     if (rawc.getCount() > 0) {
   4642                         c = new MergeCursor(new Cursor[]{rawc, vc});
   4643                     } else {
   4644                         c = rawc;
   4645                     }
   4646                 }
   4647                 break;
   4648             case UI_FULL_FOLDERS: {
   4649                 // We need a full projection for getFolderListCursor
   4650                 final String[] folderProjection = folderProjectionFromUiProjection(uiProjection);
   4651                 c = db.rawQuery(genQueryAccountAllMailboxes(folderProjection), new String[] {id});
   4652                 c = getFolderListCursor(c, Long.valueOf(id), uiProjection);
   4653                 notifyUri =
   4654                         UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build();
   4655                 break;
   4656             }
   4657             case UI_RECENT_FOLDERS:
   4658                 c = db.rawQuery(genQueryRecentMailboxes(uiProjection), new String[] {id});
   4659                 notifyUri = UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build();
   4660                 break;
   4661             case UI_SUBFOLDERS: {
   4662                 // We need a full projection for getFolderListCursor
   4663                 final String[] folderProjection = folderProjectionFromUiProjection(uiProjection);
   4664                 c = db.rawQuery(genQuerySubfolders(folderProjection), new String[] {id});
   4665                 c = getFolderListCursor(c, Mailbox.getAccountIdForMailbox(context, id),
   4666                         uiProjection);
   4667                 // Get notifications for any folder changes on this account. This is broader than
   4668                 // we need but otherwise we'd need for every folder change to notify on all relevant
   4669                 // subtrees. For now we opt for simplicity.
   4670                 final long accountId = Mailbox.getAccountIdForMailbox(context, id);
   4671                 notifyUri = ContentUris.withAppendedId(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId);
   4672                 break;
   4673             }
   4674             case UI_MESSAGES:
   4675                 long mailboxId = Long.parseLong(id);
   4676                 final Folder folder = getFolder(context, mailboxId);
   4677                 if (folder == null) {
   4678                     // This mailboxId is bogus. Return an empty cursor
   4679                     // TODO: Make callers of this query handle null cursors instead b/10819309
   4680                     return new MatrixCursor(uiProjection);
   4681                 }
   4682                 if (isVirtualMailbox(mailboxId)) {
   4683                     c = getVirtualMailboxMessagesCursor(db, uiProjection, mailboxId, unseenOnly);
   4684                 } else {
   4685                     c = db.rawQuery(
   4686                             genQueryMailboxMessages(uiProjection, unseenOnly), new String[] {id});
   4687                 }
   4688                 notifyUri = UIPROVIDER_CONVERSATION_NOTIFIER.buildUpon().appendPath(id).build();
   4689                 c = new EmailConversationCursor(context, c, folder, mailboxId);
   4690                 break;
   4691             case UI_MESSAGE:
   4692                 MessageQuery qq = genQueryViewMessage(uiProjection, id);
   4693                 String sql = qq.query;
   4694                 String attJson = qq.attachmentJson;
   4695                 // With attachments, we have another argument to bind
   4696                 if (attJson != null) {
   4697                     c = db.rawQuery(sql, new String[] {attJson, id});
   4698                 } else {
   4699                     c = db.rawQuery(sql, new String[] {id});
   4700                 }
   4701                 if (c != null) {
   4702                     c = new EmailMessageCursor(getContext(), c, UIProvider.MessageColumns.BODY_HTML,
   4703                             UIProvider.MessageColumns.BODY_TEXT);
   4704                 }
   4705                 notifyUri = UIPROVIDER_MESSAGE_NOTIFIER.buildUpon().appendPath(id).build();
   4706                 break;
   4707             case UI_ATTACHMENTS:
   4708                 final List<String> contentTypeQueryParameters =
   4709                         uri.getQueryParameters(PhotoContract.ContentTypeParameters.CONTENT_TYPE);
   4710                 c = db.rawQuery(genQueryAttachments(uiProjection, contentTypeQueryParameters),
   4711                         new String[] {id});
   4712                 c = new AttachmentsCursor(context, c);
   4713                 notifyUri = UIPROVIDER_ATTACHMENTS_NOTIFIER.buildUpon().appendPath(id).build();
   4714                 break;
   4715             case UI_ATTACHMENT:
   4716                 c = db.rawQuery(genQueryAttachment(uiProjection), new String[] {id});
   4717                 notifyUri = UIPROVIDER_ATTACHMENT_NOTIFIER.buildUpon().appendPath(id).build();
   4718                 break;
   4719             case UI_ATTACHMENT_BY_CID:
   4720                 final String cid = uri.getPathSegments().get(2);
   4721                 final String[] selectionArgs = {id, cid};
   4722                 c = db.rawQuery(genQueryAttachmentByMessageIDAndCid(uiProjection), selectionArgs);
   4723 
   4724                 // we don't have easy access to the attachment ID (which is buried in the cursor
   4725                 // being returned), so we notify on the parent message object
   4726                 notifyUri = UIPROVIDER_ATTACHMENTS_NOTIFIER.buildUpon().appendPath(id).build();
   4727                 break;
   4728             case UI_FOLDER:
   4729             case UI_INBOX:
   4730                 if (match == UI_INBOX) {
   4731                     mailboxId = Mailbox.findMailboxOfType(context, Long.parseLong(id),
   4732                             Mailbox.TYPE_INBOX);
   4733                     if (mailboxId == Mailbox.NO_MAILBOX) {
   4734                         LogUtils.d(LogUtils.TAG, "No inbox found for account %s", id);
   4735                         return null;
   4736                     }
   4737                     LogUtils.d(LogUtils.TAG, "Found inbox id %d", mailboxId);
   4738                 } else {
   4739                     mailboxId = Long.parseLong(id);
   4740                 }
   4741                 final String mailboxIdString = Long.toString(mailboxId);
   4742                 if (isVirtualMailbox(mailboxId)) {
   4743                     c = getVirtualMailboxCursor(mailboxId, uiProjection);
   4744                     notifyUri = UIPROVIDER_FOLDER_NOTIFIER.buildUpon().appendPath(mailboxIdString)
   4745                             .build();
   4746                 } else {
   4747                     c = db.rawQuery(genQueryMailbox(uiProjection, mailboxIdString),
   4748                             new String[]{mailboxIdString});
   4749                     final List<String> projectionList = Arrays.asList(uiProjection);
   4750                     final int nameColumn = projectionList.indexOf(UIProvider.FolderColumns.NAME);
   4751                     final int typeColumn = projectionList.indexOf(UIProvider.FolderColumns.TYPE);
   4752                     if (c.moveToFirst()) {
   4753                         final Cursor closeThis = c;
   4754                         try {
   4755                             c = getUiFolderCursorRowFromMailboxCursorRow(
   4756                                     new MatrixCursorWithCachedColumns(uiProjection),
   4757                                     uiProjection.length, c, nameColumn, typeColumn);
   4758                         } finally {
   4759                             closeThis.close();
   4760                         }
   4761                     }
   4762                     notifyUri = UIPROVIDER_FOLDER_NOTIFIER.buildUpon().appendPath(mailboxIdString)
   4763                             .build();
   4764                 }
   4765                 break;
   4766             case UI_ACCOUNT:
   4767                 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) {
   4768                     MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection, 1);
   4769                     addCombinedAccountRow(mc);
   4770                     c = mc;
   4771                 } else {
   4772                     c = db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id});
   4773                 }
   4774                 notifyUri = UIPROVIDER_ACCOUNT_NOTIFIER.buildUpon().appendPath(id).build();
   4775                 break;
   4776             case UI_CONVERSATION:
   4777                 c = db.rawQuery(genQueryConversation(uiProjection), new String[] {id});
   4778                 break;
   4779         }
   4780         if (notifyUri != null) {
   4781             c.setNotificationUri(resolver, notifyUri);
   4782         }
   4783         return c;
   4784     }
   4785 
   4786     /**
   4787      * Convert a UIProvider attachment to an EmailProvider attachment (for sending); we only need
   4788      * a few of the fields
   4789      * @param uiAtt the UIProvider attachment to convert
   4790      * @param cachedFile the path to the cached file to
   4791      * @return the EmailProvider attachment
   4792      */
   4793     // TODO(pwestbro): once the Attachment contains the cached uri, the second parameter can be
   4794     // removed
   4795     // TODO(mhibdon): if the UI Attachment contained the account key, the third parameter could
   4796     // be removed.
   4797     private static Attachment convertUiAttachmentToAttachment(
   4798             com.android.mail.providers.Attachment uiAtt, String cachedFile, long accountKey) {
   4799         final Attachment att = new Attachment();
   4800 
   4801         att.setContentUri(uiAtt.contentUri.toString());
   4802 
   4803         if (!TextUtils.isEmpty(cachedFile)) {
   4804             // Generate the content provider uri for this cached file
   4805             final Uri.Builder cachedFileBuilder = Uri.parse(
   4806                     "content://" + EmailContent.AUTHORITY + "/attachment/cachedFile").buildUpon();
   4807             cachedFileBuilder.appendQueryParameter(Attachment.CACHED_FILE_QUERY_PARAM, cachedFile);
   4808             att.setCachedFileUri(cachedFileBuilder.build().toString());
   4809         }
   4810         att.mAccountKey = accountKey;
   4811         att.mFileName = uiAtt.getName();
   4812         att.mMimeType = uiAtt.getContentType();
   4813         att.mSize = uiAtt.size;
   4814         return att;
   4815     }
   4816 
   4817     /**
   4818      * Create a mailbox given the account and mailboxType.
   4819      */
   4820     private Mailbox createMailbox(long accountId, int mailboxType) {
   4821         Context context = getContext();
   4822         Mailbox box = Mailbox.newSystemMailbox(context, accountId, mailboxType);
   4823         // Make sure drafts and save will show up in recents...
   4824         // If these already exist (from old Email app), they will have touch times
   4825         switch (mailboxType) {
   4826             case Mailbox.TYPE_DRAFTS:
   4827                 box.mLastTouchedTime = Mailbox.DRAFTS_DEFAULT_TOUCH_TIME;
   4828                 break;
   4829             case Mailbox.TYPE_SENT:
   4830                 box.mLastTouchedTime = Mailbox.SENT_DEFAULT_TOUCH_TIME;
   4831                 break;
   4832         }
   4833         box.save(context);
   4834         return box;
   4835     }
   4836 
   4837     /**
   4838      * Given an account name and a mailbox type, return that mailbox, creating it if necessary
   4839      * @param accountId the account id to use
   4840      * @param mailboxType the type of mailbox we're trying to find
   4841      * @return the mailbox of the given type for the account in the uri, or null if not found
   4842      */
   4843     private Mailbox getMailboxByAccountIdAndType(final long accountId, final int mailboxType) {
   4844         Mailbox mailbox = Mailbox.restoreMailboxOfType(getContext(), accountId, mailboxType);
   4845         if (mailbox == null) {
   4846             mailbox = createMailbox(accountId, mailboxType);
   4847         }
   4848         return mailbox;
   4849     }
   4850 
   4851     /**
   4852      * Given a mailbox and the content values for a message, create/save the message in the mailbox
   4853      * @param mailbox the mailbox to use
   4854      * @param extras the bundle containing the message fields
   4855      * @return the uri of the newly created message
   4856      * TODO(yph): The following fields are available in extras but unused, verify whether they
   4857      *     should be respected:
   4858      *     - UIProvider.MessageColumns.SNIPPET
   4859      *     - UIProvider.MessageColumns.REPLY_TO
   4860      *     - UIProvider.MessageColumns.FROM
   4861      */
   4862     private Uri uiSaveMessage(Message msg, Mailbox mailbox, Bundle extras) {
   4863         final Context context = getContext();
   4864         // Fill in the message
   4865         final Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey);
   4866         if (account == null) return null;
   4867         final String customFromAddress =
   4868                 extras.getString(UIProvider.MessageColumns.CUSTOM_FROM_ADDRESS);
   4869         if (!TextUtils.isEmpty(customFromAddress)) {
   4870             msg.mFrom = customFromAddress;
   4871         } else {
   4872             msg.mFrom = account.getEmailAddress();
   4873         }
   4874         msg.mTimeStamp = System.currentTimeMillis();
   4875         msg.mTo = extras.getString(UIProvider.MessageColumns.TO);
   4876         msg.mCc = extras.getString(UIProvider.MessageColumns.CC);
   4877         msg.mBcc = extras.getString(UIProvider.MessageColumns.BCC);
   4878         msg.mSubject = extras.getString(UIProvider.MessageColumns.SUBJECT);
   4879         msg.mText = extras.getString(UIProvider.MessageColumns.BODY_TEXT);
   4880         msg.mHtml = extras.getString(UIProvider.MessageColumns.BODY_HTML);
   4881         msg.mMailboxKey = mailbox.mId;
   4882         msg.mAccountKey = mailbox.mAccountKey;
   4883         msg.mDisplayName = msg.mTo;
   4884         msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE;
   4885         msg.mFlagRead = true;
   4886         msg.mFlagSeen = true;
   4887         msg.mQuotedTextStartPos = extras.getInt(UIProvider.MessageColumns.QUOTE_START_POS, 0);
   4888         int flags = 0;
   4889         final int draftType = extras.getInt(UIProvider.MessageColumns.DRAFT_TYPE);
   4890         switch(draftType) {
   4891             case DraftType.FORWARD:
   4892                 flags |= Message.FLAG_TYPE_FORWARD;
   4893                 break;
   4894             case DraftType.REPLY_ALL:
   4895                 flags |= Message.FLAG_TYPE_REPLY_ALL;
   4896                 //$FALL-THROUGH$
   4897             case DraftType.REPLY:
   4898                 flags |= Message.FLAG_TYPE_REPLY;
   4899                 break;
   4900             case DraftType.COMPOSE:
   4901                 flags |= Message.FLAG_TYPE_ORIGINAL;
   4902                 break;
   4903         }
   4904         int draftInfo = 0;
   4905         if (extras.containsKey(UIProvider.MessageColumns.QUOTE_START_POS)) {
   4906             draftInfo = extras.getInt(UIProvider.MessageColumns.QUOTE_START_POS);
   4907             if (extras.getInt(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT) != 0) {
   4908                 draftInfo |= Message.DRAFT_INFO_APPEND_REF_MESSAGE;
   4909             }
   4910         }
   4911         if (!extras.containsKey(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT)) {
   4912             flags |= Message.FLAG_NOT_INCLUDE_QUOTED_TEXT;
   4913         }
   4914         msg.mDraftInfo = draftInfo;
   4915         msg.mFlags = flags;
   4916 
   4917         final String ref = extras.getString(UIProvider.MessageColumns.REF_MESSAGE_ID);
   4918         if (ref != null && msg.mQuotedTextStartPos >= 0) {
   4919             String refId = Uri.parse(ref).getLastPathSegment();
   4920             try {
   4921                 msg.mSourceKey = Long.parseLong(refId);
   4922             } catch (NumberFormatException e) {
   4923                 // This will be zero; the default
   4924             }
   4925         }
   4926 
   4927         // Get attachments from the ContentValues
   4928         final List<com.android.mail.providers.Attachment> uiAtts =
   4929                 com.android.mail.providers.Attachment.fromJSONArray(
   4930                         extras.getString(UIProvider.MessageColumns.ATTACHMENTS));
   4931         final ArrayList<Attachment> atts = new ArrayList<Attachment>();
   4932         boolean hasUnloadedAttachments = false;
   4933         Bundle attachmentFds =
   4934                 extras.getParcelable(UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP);
   4935         for (com.android.mail.providers.Attachment uiAtt: uiAtts) {
   4936             final Uri attUri = uiAtt.uri;
   4937             if (attUri != null && attUri.getAuthority().equals(EmailContent.AUTHORITY)) {
   4938                 // If it's one of ours, retrieve the attachment and add it to the list
   4939                 final long attId = Long.parseLong(attUri.getLastPathSegment());
   4940                 final Attachment att = Attachment.restoreAttachmentWithId(context, attId);
   4941                 if (att != null) {
   4942                     // We must clone the attachment into a new one for this message; easiest to
   4943                     // use a parcel here
   4944                     final Parcel p = Parcel.obtain();
   4945                     att.writeToParcel(p, 0);
   4946                     p.setDataPosition(0);
   4947                     final Attachment attClone = new Attachment(p);
   4948                     p.recycle();
   4949                     // Clear the messageKey (this is going to be a new attachment)
   4950                     attClone.mMessageKey = 0;
   4951                     // If we're sending this, it's not loaded, and we're not smart forwarding
   4952                     // add the download flag, so that ADS will start up
   4953                     if (mailbox.mType == Mailbox.TYPE_OUTBOX && att.getContentUri() == null &&
   4954                             ((account.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) == 0)) {
   4955                         attClone.mFlags |= Attachment.FLAG_DOWNLOAD_FORWARD;
   4956                         hasUnloadedAttachments = true;
   4957                     }
   4958                     atts.add(attClone);
   4959                 }
   4960             } else {
   4961                 // Cache the attachment.  This will allow us to send it, if the permissions are
   4962                 // revoked.
   4963                 final String cachedFileUri =
   4964                         AttachmentUtils.cacheAttachmentUri(context, uiAtt, attachmentFds);
   4965 
   4966                 // Convert external attachment to one of ours and add to the list
   4967                 atts.add(convertUiAttachmentToAttachment(uiAtt, cachedFileUri, msg.mAccountKey));
   4968             }
   4969         }
   4970         if (!atts.isEmpty()) {
   4971             msg.mAttachments = atts;
   4972             msg.mFlagAttachment = true;
   4973             if (hasUnloadedAttachments) {
   4974                 Utility.showToast(context, R.string.message_view_attachment_background_load);
   4975             }
   4976         }
   4977         // Save it or update it...
   4978         if (!msg.isSaved()) {
   4979             msg.save(context);
   4980         } else {
   4981             // This is tricky due to how messages/attachments are saved; rather than putz with
   4982             // what's changed, we'll delete/re-add them
   4983             final ArrayList<ContentProviderOperation> ops =
   4984                     new ArrayList<ContentProviderOperation>();
   4985             // Delete all existing attachments
   4986             ops.add(ContentProviderOperation.newDelete(
   4987                     ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, msg.mId))
   4988                     .build());
   4989             // Delete the body
   4990             ops.add(ContentProviderOperation.newDelete(Body.CONTENT_URI)
   4991                     .withSelection(BodyColumns.MESSAGE_KEY + "=?",
   4992                             new String[] {Long.toString(msg.mId)})
   4993                     .build());
   4994             // Add the ops for the message, atts, and body
   4995             msg.addSaveOps(ops);
   4996             // Do it!
   4997             try {
   4998                 applyBatch(ops);
   4999             } catch (OperationApplicationException e) {
   5000                 LogUtils.d(TAG, "applyBatch exception");
   5001             }
   5002         }
   5003         notifyUIMessage(msg.mId);
   5004 
   5005         if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
   5006             startSync(mailbox, 0);
   5007             final long originalMsgId = msg.mSourceKey;
   5008             if (originalMsgId != 0) {
   5009                 final Message originalMsg = Message.restoreMessageWithId(context, originalMsgId);
   5010                 // If the original message exists, set its forwarded/replied to flags
   5011                 if (originalMsg != null) {
   5012                     final ContentValues cv = new ContentValues();
   5013                     flags = originalMsg.mFlags;
   5014                     switch(draftType) {
   5015                         case DraftType.FORWARD:
   5016                             flags |= Message.FLAG_FORWARDED;
   5017                             break;
   5018                         case DraftType.REPLY_ALL:
   5019                         case DraftType.REPLY:
   5020                             flags |= Message.FLAG_REPLIED_TO;
   5021                             break;
   5022                     }
   5023                     cv.put(MessageColumns.FLAGS, flags);
   5024                     context.getContentResolver().update(ContentUris.withAppendedId(
   5025                             Message.CONTENT_URI, originalMsgId), cv, null, null);
   5026                 }
   5027             }
   5028         }
   5029         return uiUri("uimessage", msg.mId);
   5030     }
   5031 
   5032     private Uri uiSaveDraftMessage(final long accountId, final Bundle extras) {
   5033         final Mailbox mailbox =
   5034                 getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_DRAFTS);
   5035         if (mailbox == null) return null;
   5036         Message msg = null;
   5037         if (extras.containsKey(BaseColumns._ID)) {
   5038             final long messageId = extras.getLong(BaseColumns._ID);
   5039             msg = Message.restoreMessageWithId(getContext(), messageId);
   5040         }
   5041         if (msg == null) {
   5042             msg = new Message();
   5043         }
   5044         return uiSaveMessage(msg, mailbox, extras);
   5045     }
   5046 
   5047     private Uri uiSendDraftMessage(final long accountId, final Bundle extras) {
   5048         final Message msg;
   5049         if (extras.containsKey(BaseColumns._ID)) {
   5050             final long messageId = extras.getLong(BaseColumns._ID);
   5051             msg = Message.restoreMessageWithId(getContext(), messageId);
   5052         } else {
   5053             msg = new Message();
   5054         }
   5055 
   5056         if (msg == null) return null;
   5057         final Mailbox mailbox = getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_OUTBOX);
   5058         if (mailbox == null) return null;
   5059         // Make sure the sent mailbox exists, since it will be necessary soon.
   5060         // TODO(yph): move system mailbox creation to somewhere sane.
   5061         final Mailbox sentMailbox = getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_SENT);
   5062         if (sentMailbox == null) return null;
   5063         final Uri messageUri = uiSaveMessage(msg, mailbox, extras);
   5064         // Kick observers
   5065         notifyUI(Mailbox.CONTENT_URI, null);
   5066         return messageUri;
   5067     }
   5068 
   5069     private static void putIntegerLongOrBoolean(ContentValues values, String columnName,
   5070             Object value) {
   5071         if (value instanceof Integer) {
   5072             Integer intValue = (Integer)value;
   5073             values.put(columnName, intValue);
   5074         } else if (value instanceof Boolean) {
   5075             Boolean boolValue = (Boolean)value;
   5076             values.put(columnName, boolValue ? 1 : 0);
   5077         } else if (value instanceof Long) {
   5078             Long longValue = (Long)value;
   5079             values.put(columnName, longValue);
   5080         }
   5081     }
   5082 
   5083     /**
   5084      * Update the timestamps for the folders specified and notifies on the recent folder URI.
   5085      * @param folders array of folder Uris to update
   5086      * @return number of folders updated
   5087      */
   5088     private int updateTimestamp(final Context context, String id, Uri[] folders){
   5089         int updated = 0;
   5090         final long now = System.currentTimeMillis();
   5091         final ContentResolver resolver = context.getContentResolver();
   5092         final ContentValues touchValues = new ContentValues(1);
   5093         for (final Uri folder : folders) {
   5094             touchValues.put(MailboxColumns.LAST_TOUCHED_TIME, now);
   5095             LogUtils.d(TAG, "updateStamp: %s updated", folder);
   5096             updated += resolver.update(folder, touchValues, null, null);
   5097         }
   5098         final Uri toNotify =
   5099                 UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build();
   5100         LogUtils.d(TAG, "updateTimestamp: Notifying on %s", toNotify);
   5101         notifyUI(toNotify, null);
   5102         return updated;
   5103     }
   5104 
   5105     /**
   5106      * Updates the recent folders. The values to be updated are specified as ContentValues pairs
   5107      * of (Folder URI, access timestamp). Returns nonzero if successful, always.
   5108      * @param uri provider query uri
   5109      * @param values uri, timestamp pairs
   5110      * @return nonzero value always.
   5111      */
   5112     private int uiUpdateRecentFolders(Uri uri, ContentValues values) {
   5113         final int numFolders = values.size();
   5114         final String id = uri.getPathSegments().get(1);
   5115         final Uri[] folders = new Uri[numFolders];
   5116         final Context context = getContext();
   5117         int i = 0;
   5118         for (final String uriString : values.keySet()) {
   5119             folders[i] = Uri.parse(uriString);
   5120         }
   5121         return updateTimestamp(context, id, folders);
   5122     }
   5123 
   5124     /**
   5125      * Populates the recent folders according to the design.
   5126      * @param uri provider query uri
   5127      * @return the number of recent folders were populated.
   5128      */
   5129     private int uiPopulateRecentFolders(Uri uri) {
   5130         final Context context = getContext();
   5131         final String id = uri.getLastPathSegment();
   5132         final Uri[] recentFolders = defaultRecentFolders(id);
   5133         final int numFolders = recentFolders.length;
   5134         if (numFolders <= 0) {
   5135             return 0;
   5136         }
   5137         final int rowsUpdated = updateTimestamp(context, id, recentFolders);
   5138         LogUtils.d(TAG, "uiPopulateRecentFolders: %d folders changed", rowsUpdated);
   5139         return rowsUpdated;
   5140     }
   5141 
   5142     private int uiUpdateAttachment(Uri uri, ContentValues uiValues) {
   5143         int result = 0;
   5144         Integer stateValue = uiValues.getAsInteger(UIProvider.AttachmentColumns.STATE);
   5145         if (stateValue != null) {
   5146             // This is a command from UIProvider
   5147             long attachmentId = Long.parseLong(uri.getLastPathSegment());
   5148             Context context = getContext();
   5149             Attachment attachment =
   5150                     Attachment.restoreAttachmentWithId(context, attachmentId);
   5151             if (attachment == null) {
   5152                 // Went away; ah, well...
   5153                 return result;
   5154             }
   5155             int state = stateValue;
   5156             ContentValues values = new ContentValues();
   5157             if (state == UIProvider.AttachmentState.NOT_SAVED
   5158                     || state == UIProvider.AttachmentState.REDOWNLOADING) {
   5159                 // Set state, try to cancel request
   5160                 values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.NOT_SAVED);
   5161                 values.put(AttachmentColumns.FLAGS,
   5162                         attachment.mFlags &= ~Attachment.FLAG_DOWNLOAD_USER_REQUEST);
   5163                 attachment.update(context, values);
   5164                 result = 1;
   5165             }
   5166             if (state == UIProvider.AttachmentState.DOWNLOADING
   5167                     || state == UIProvider.AttachmentState.REDOWNLOADING) {
   5168                 // Set state and destination; request download
   5169                 values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.DOWNLOADING);
   5170                 Integer destinationValue =
   5171                         uiValues.getAsInteger(UIProvider.AttachmentColumns.DESTINATION);
   5172                 values.put(AttachmentColumns.UI_DESTINATION,
   5173                         destinationValue == null ? 0 : destinationValue);
   5174                 values.put(AttachmentColumns.FLAGS,
   5175                         attachment.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST);
   5176 
   5177                 if (values.containsKey(AttachmentColumns.LOCATION) &&
   5178                         TextUtils.isEmpty(values.getAsString(AttachmentColumns.LOCATION))) {
   5179                     LogUtils.w(TAG, new Throwable(), "attachment with blank location");
   5180                 }
   5181 
   5182                 attachment.update(context, values);
   5183                 result = 1;
   5184             }
   5185             if (state == UIProvider.AttachmentState.SAVED) {
   5186                 // If this is an inline attachment, notify message has changed
   5187                 if (!TextUtils.isEmpty(attachment.mContentId)) {
   5188                     notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, attachment.mMessageKey);
   5189                 }
   5190                 result = 1;
   5191             }
   5192         }
   5193         return result;
   5194     }
   5195 
   5196     private int uiUpdateFolder(final Context context, Uri uri, ContentValues uiValues) {
   5197         // We need to mark seen separately
   5198         if (uiValues.containsKey(UIProvider.ConversationColumns.SEEN)) {
   5199             final int seenValue = uiValues.getAsInteger(UIProvider.ConversationColumns.SEEN);
   5200 
   5201             if (seenValue == 1) {
   5202                 final String mailboxId = uri.getLastPathSegment();
   5203                 final int rows = markAllSeen(context, mailboxId);
   5204 
   5205                 if (uiValues.size() == 1) {
   5206                     // Nothing else to do, so return this value
   5207                     return rows;
   5208                 }
   5209             }
   5210         }
   5211 
   5212         final Uri ourUri = convertToEmailProviderUri(uri, Mailbox.CONTENT_URI, true);
   5213         if (ourUri == null) return 0;
   5214         ContentValues ourValues = new ContentValues();
   5215         // This should only be called via update to "recent folders"
   5216         for (String columnName: uiValues.keySet()) {
   5217             if (columnName.equals(MailboxColumns.LAST_TOUCHED_TIME)) {
   5218                 ourValues.put(MailboxColumns.LAST_TOUCHED_TIME, uiValues.getAsLong(columnName));
   5219             }
   5220         }
   5221         return update(ourUri, ourValues, null, null);
   5222     }
   5223 
   5224     private int uiUpdateSettings(final Context c, final ContentValues uiValues) {
   5225         final MailPrefs mailPrefs = MailPrefs.get(c);
   5226 
   5227         if (uiValues.containsKey(SettingsColumns.AUTO_ADVANCE)) {
   5228             mailPrefs.setAutoAdvanceMode(uiValues.getAsInteger(SettingsColumns.AUTO_ADVANCE));
   5229         }
   5230         if (uiValues.containsKey(SettingsColumns.CONVERSATION_VIEW_MODE)) {
   5231             final int value = uiValues.getAsInteger(SettingsColumns.CONVERSATION_VIEW_MODE);
   5232             final boolean overviewMode = value == UIProvider.ConversationViewMode.OVERVIEW;
   5233             mailPrefs.setConversationOverviewMode(overviewMode);
   5234         }
   5235 
   5236         c.getContentResolver().notifyChange(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null, false);
   5237 
   5238         return 1;
   5239     }
   5240 
   5241     private int markAllSeen(final Context context, final String mailboxId) {
   5242         final SQLiteDatabase db = getDatabase(context);
   5243         final String table = Message.TABLE_NAME;
   5244         final ContentValues values = new ContentValues(1);
   5245         values.put(MessageColumns.FLAG_SEEN, 1);
   5246         final String whereClause = MessageColumns.MAILBOX_KEY + " = ?";
   5247         final String[] whereArgs = new String[] {mailboxId};
   5248 
   5249         return db.update(table, values, whereClause, whereArgs);
   5250     }
   5251 
   5252     private ContentValues convertUiMessageValues(Message message, ContentValues values) {
   5253         final ContentValues ourValues = new ContentValues();
   5254         for (String columnName : values.keySet()) {
   5255             final Object val = values.get(columnName);
   5256             if (columnName.equals(UIProvider.ConversationColumns.STARRED)) {
   5257                 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_FAVORITE, val);
   5258             } else if (columnName.equals(UIProvider.ConversationColumns.READ)) {
   5259                 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_READ, val);
   5260             } else if (columnName.equals(UIProvider.ConversationColumns.SEEN)) {
   5261                 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_SEEN, val);
   5262             } else if (columnName.equals(MessageColumns.MAILBOX_KEY)) {
   5263                 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, val);
   5264             } else if (columnName.equals(UIProvider.ConversationOperations.FOLDERS_UPDATED)) {
   5265                 // Skip this column, as the folders will also be specified  the RAW_FOLDERS column
   5266             } else if (columnName.equals(UIProvider.ConversationColumns.RAW_FOLDERS)) {
   5267                 // Convert from folder list uri to mailbox key
   5268                 final FolderList flist = FolderList.fromBlob(values.getAsByteArray(columnName));
   5269                 if (flist.folders.size() != 1) {
   5270                     LogUtils.e(TAG,
   5271                             "Incorrect number of folders for this message: Message is %s",
   5272                             message.mId);
   5273                 } else {
   5274                     final Folder f = flist.folders.get(0);
   5275                     final Uri uri = f.folderUri.fullUri;
   5276                     final Long mailboxId = Long.parseLong(uri.getLastPathSegment());
   5277                     putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, mailboxId);
   5278                 }
   5279             } else if (columnName.equals(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES)) {
   5280                 Address[] fromList = Address.fromHeader(message.mFrom);
   5281                 final MailPrefs mailPrefs = MailPrefs.get(getContext());
   5282                 for (Address sender : fromList) {
   5283                     final String email = sender.getAddress();
   5284                     mailPrefs.setDisplayImagesFromSender(email, null);
   5285                 }
   5286             } else if (columnName.equals(UIProvider.ConversationColumns.VIEWED) ||
   5287                     columnName.equals(UIProvider.ConversationOperations.Parameters.SUPPRESS_UNDO)) {
   5288                 // Ignore for now
   5289             } else if (UIProvider.ConversationColumns.CONVERSATION_INFO.equals(columnName)) {
   5290                 // Email's conversation info is generated, not stored, so just ignore this update
   5291             } else {
   5292                 throw new IllegalArgumentException("Can't update " + columnName + " in message");
   5293             }
   5294         }
   5295         return ourValues;
   5296     }
   5297 
   5298     private static Uri convertToEmailProviderUri(Uri uri, Uri newBaseUri, boolean asProvider) {
   5299         final String idString = uri.getLastPathSegment();
   5300         try {
   5301             final long id = Long.parseLong(idString);
   5302             Uri ourUri = ContentUris.withAppendedId(newBaseUri, id);
   5303             if (asProvider) {
   5304                 ourUri = ourUri.buildUpon().appendQueryParameter(IS_UIPROVIDER, "true").build();
   5305             }
   5306             return ourUri;
   5307         } catch (NumberFormatException e) {
   5308             return null;
   5309         }
   5310     }
   5311 
   5312     private Message getMessageFromLastSegment(Uri uri) {
   5313         long messageId = Long.parseLong(uri.getLastPathSegment());
   5314         return Message.restoreMessageWithId(getContext(), messageId);
   5315     }
   5316 
   5317     /**
   5318      * Add an undo operation for the current sequence; if the sequence is newer than what we've had,
   5319      * clear out the undo list and start over
   5320      * @param uri the uri we're working on
   5321      * @param op the ContentProviderOperation to perform upon undo
   5322      */
   5323     private void addToSequence(Uri uri, ContentProviderOperation op) {
   5324         String sequenceString = uri.getQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER);
   5325         if (sequenceString != null) {
   5326             int sequence = Integer.parseInt(sequenceString);
   5327             if (sequence > mLastSequence) {
   5328                 // Reset sequence
   5329                 mLastSequenceOps.clear();
   5330                 mLastSequence = sequence;
   5331             }
   5332             // TODO: Need something to indicate a change isn't ready (undoable)
   5333             mLastSequenceOps.add(op);
   5334         }
   5335     }
   5336 
   5337     // TODO: This should depend on flags on the mailbox...
   5338     private static boolean uploadsToServer(Context context, Mailbox m) {
   5339         if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX ||
   5340                 m.mType == Mailbox.TYPE_SEARCH) {
   5341             return false;
   5342         }
   5343         String protocol = Account.getProtocol(context, m.mAccountKey);
   5344         EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol);
   5345         return (info != null && info.syncChanges);
   5346     }
   5347 
   5348     private int uiUpdateMessage(Uri uri, ContentValues values) {
   5349         return uiUpdateMessage(uri, values, false);
   5350     }
   5351 
   5352     private int uiUpdateMessage(Uri uri, ContentValues values, boolean forceSync) {
   5353         Context context = getContext();
   5354         Message msg = getMessageFromLastSegment(uri);
   5355         if (msg == null) return 0;
   5356         Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey);
   5357         if (mailbox == null) return 0;
   5358         Uri ourBaseUri =
   5359                 (forceSync || uploadsToServer(context, mailbox)) ? Message.SYNCED_CONTENT_URI :
   5360                     Message.CONTENT_URI;
   5361         Uri ourUri = convertToEmailProviderUri(uri, ourBaseUri, true);
   5362         if (ourUri == null) return 0;
   5363 
   5364         // Special case - meeting response
   5365         if (values.containsKey(UIProvider.MessageOperations.RESPOND_COLUMN)) {
   5366             final EmailServiceProxy service =
   5367                     EmailServiceUtils.getServiceForAccount(context, mailbox.mAccountKey);
   5368             try {
   5369                 service.sendMeetingResponse(msg.mId,
   5370                         values.getAsInteger(UIProvider.MessageOperations.RESPOND_COLUMN));
   5371                 // Delete the message immediately
   5372                 uiDeleteMessage(uri);
   5373                 Utility.showToast(context, R.string.confirm_response);
   5374                 // Notify box has changed so the deletion is reflected in the UI
   5375                 notifyUIConversationMailbox(mailbox.mId);
   5376             } catch (RemoteException e) {
   5377                 LogUtils.d(TAG, "Remote exception while sending meeting response");
   5378             }
   5379             return 1;
   5380         }
   5381 
   5382         // Another special case - deleting a draft.
   5383         final String operation = values.getAsString(
   5384                 UIProvider.ConversationOperations.OPERATION_KEY);
   5385         // TODO: for now let's just default to delete for MOVE_FAILED_TO_DRAFT operation
   5386         if (UIProvider.ConversationOperations.DISCARD_DRAFTS.equals(operation) ||
   5387                 UIProvider.ConversationOperations.MOVE_FAILED_TO_DRAFTS.equals(operation)) {
   5388             uiDeleteMessage(uri);
   5389             return 1;
   5390         }
   5391 
   5392         ContentValues undoValues = new ContentValues();
   5393         ContentValues ourValues = convertUiMessageValues(msg, values);
   5394         for (String columnName: ourValues.keySet()) {
   5395             if (columnName.equals(MessageColumns.MAILBOX_KEY)) {
   5396                 undoValues.put(MessageColumns.MAILBOX_KEY, msg.mMailboxKey);
   5397             } else if (columnName.equals(MessageColumns.FLAG_READ)) {
   5398                 undoValues.put(MessageColumns.FLAG_READ, msg.mFlagRead);
   5399             } else if (columnName.equals(MessageColumns.FLAG_SEEN)) {
   5400                 undoValues.put(MessageColumns.FLAG_SEEN, msg.mFlagSeen);
   5401             } else if (columnName.equals(MessageColumns.FLAG_FAVORITE)) {
   5402                 undoValues.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite);
   5403             }
   5404         }
   5405         if (undoValues.size() == 0) {
   5406             return -1;
   5407         }
   5408         final Boolean suppressUndo =
   5409                 values.getAsBoolean(UIProvider.ConversationOperations.Parameters.SUPPRESS_UNDO);
   5410         if (suppressUndo == null || !suppressUndo) {
   5411             final ContentProviderOperation op =
   5412                     ContentProviderOperation.newUpdate(convertToEmailProviderUri(
   5413                             uri, ourBaseUri, false))
   5414                             .withValues(undoValues)
   5415                             .build();
   5416             addToSequence(uri, op);
   5417         }
   5418 
   5419         return update(ourUri, ourValues, null, null);
   5420     }
   5421 
   5422     /**
   5423      * Projection for use with getting mailbox & account keys for a message.
   5424      */
   5425     private static final String[] MESSAGE_KEYS_PROJECTION =
   5426             { MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY };
   5427     private static final int MESSAGE_KEYS_MAILBOX_KEY_COLUMN = 0;
   5428     private static final int MESSAGE_KEYS_ACCOUNT_KEY_COLUMN = 1;
   5429 
   5430     /**
   5431      * Notify necessary UI components in response to a message update.
   5432      * @param uri The {@link Uri} for this message update.
   5433      * @param messageId The id of the message that's been updated.
   5434      * @param values The {@link ContentValues} that were updated in the message.
   5435      */
   5436     private void handleMessageUpdateNotifications(final Uri uri, final String messageId,
   5437             final ContentValues values) {
   5438         if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
   5439             notifyUIConversation(uri);
   5440         }
   5441         notifyUIMessage(messageId);
   5442         // TODO: Ideally, also test that the values actually changed.
   5443         if (values.containsKey(MessageColumns.FLAG_READ) ||
   5444                 values.containsKey(MessageColumns.MAILBOX_KEY)) {
   5445             final Cursor c = query(
   5446                     Message.CONTENT_URI.buildUpon().appendEncodedPath(messageId).build(),
   5447                     MESSAGE_KEYS_PROJECTION, null, null, null);
   5448             if (c != null) {
   5449                 try {
   5450                     if (c.moveToFirst()) {
   5451                         notifyUIFolder(c.getLong(MESSAGE_KEYS_MAILBOX_KEY_COLUMN),
   5452                                 c.getLong(MESSAGE_KEYS_ACCOUNT_KEY_COLUMN));
   5453                     }
   5454                 } finally {
   5455                     c.close();
   5456                 }
   5457             }
   5458         }
   5459     }
   5460 
   5461     /**
   5462      * Perform a "Delete" operation
   5463      * @param uri message to delete
   5464      * @return number of rows affected
   5465      */
   5466     private int uiDeleteMessage(Uri uri) {
   5467         final Context context = getContext();
   5468         Message msg = getMessageFromLastSegment(uri);
   5469         if (msg == null) return 0;
   5470         Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey);
   5471         if (mailbox == null) return 0;
   5472         if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_DRAFTS) {
   5473             // We actually delete these, including attachments
   5474             AttachmentUtilities.deleteAllAttachmentFiles(context, msg.mAccountKey, msg.mId);
   5475             final int r = context.getContentResolver().delete(
   5476                     ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, msg.mId), null, null);
   5477             notifyUIFolder(mailbox.mId, mailbox.mAccountKey);
   5478             notifyUIMessage(msg.mId);
   5479             return r;
   5480         }
   5481         Mailbox trashMailbox =
   5482                 Mailbox.restoreMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_TRASH);
   5483         if (trashMailbox == null) {
   5484             return 0;
   5485         }
   5486         ContentValues values = new ContentValues();
   5487         values.put(MessageColumns.MAILBOX_KEY, trashMailbox.mId);
   5488         final int r = uiUpdateMessage(uri, values, true);
   5489         notifyUIFolder(mailbox.mId, mailbox.mAccountKey);
   5490         notifyUIMessage(msg.mId);
   5491         return r;
   5492     }
   5493 
   5494     /**
   5495      * Hard delete all synced messages in a particular mailbox
   5496      * @param uri Mailbox to empty (Trash, or maybe Spam/Junk later)
   5497      * @return number of rows affected
   5498      */
   5499     private int uiPurgeFolder(Uri uri) {
   5500         final Context context = getContext();
   5501         final long mailboxId = Long.parseLong(uri.getLastPathSegment());
   5502         final SQLiteDatabase db = getDatabase(context);
   5503 
   5504         // Find the account ID (needed in a few calls)
   5505         final Cursor mailboxCursor = db.query(
   5506                 Mailbox.TABLE_NAME, new String[] { MailboxColumns.ACCOUNT_KEY },
   5507                 Mailbox._ID + "=" + mailboxId, null, null, null, null);
   5508         if (mailboxCursor == null || !mailboxCursor.moveToFirst()) {
   5509             LogUtils.wtf(LogUtils.TAG, "Null or empty cursor when trying to purge mailbox %d",
   5510                     mailboxId);
   5511             return 0;
   5512         }
   5513         final long accountId = mailboxCursor.getLong(mailboxCursor.getColumnIndex(
   5514                 MailboxColumns.ACCOUNT_KEY));
   5515 
   5516         // Find all the messages in the mailbox
   5517         final String[] messageProjection =
   5518                 new String[] { MessageColumns._ID };
   5519         final String messageWhere = MessageColumns.MAILBOX_KEY + "=" + mailboxId;
   5520         final Cursor messageCursor = db.query(Message.TABLE_NAME, messageProjection, messageWhere,
   5521                 null, null, null, null);
   5522         int deletedCount = 0;
   5523 
   5524         // Kill them with fire
   5525         while (messageCursor != null && messageCursor.moveToNext()) {
   5526             final long messageId = messageCursor.getLong(messageCursor.getColumnIndex(
   5527                     MessageColumns._ID));
   5528             AttachmentUtilities.deleteAllAttachmentFiles(context, accountId, messageId);
   5529             deletedCount += context.getContentResolver().delete(
   5530                     ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, messageId), null, null);
   5531             notifyUIMessage(messageId);
   5532         }
   5533 
   5534         notifyUIFolder(mailboxId, accountId);
   5535         return deletedCount;
   5536     }
   5537 
   5538     public static final String PICKER_UI_ACCOUNT = "picker_ui_account";
   5539     public static final String PICKER_MAILBOX_TYPE = "picker_mailbox_type";
   5540     // Currently unused
   5541     //public static final String PICKER_MESSAGE_ID = "picker_message_id";
   5542     public static final String PICKER_HEADER_ID = "picker_header_id";
   5543 
   5544     private int pickFolder(Uri uri, int type, int headerId) {
   5545         Context context = getContext();
   5546         Long acctId = Long.parseLong(uri.getLastPathSegment());
   5547         // For push imap, for example, we want the user to select the trash mailbox
   5548         Cursor ac = query(uiUri("uiaccount", acctId), UIProvider.ACCOUNTS_PROJECTION,
   5549                 null, null, null);
   5550         try {
   5551             if (ac.moveToFirst()) {
   5552                 final com.android.mail.providers.Account uiAccount =
   5553                         com.android.mail.providers.Account.builder().buildFrom(ac);
   5554                 Intent intent = new Intent(context, FolderPickerActivity.class);
   5555                 intent.putExtra(PICKER_UI_ACCOUNT, uiAccount);
   5556                 intent.putExtra(PICKER_MAILBOX_TYPE, type);
   5557                 intent.putExtra(PICKER_HEADER_ID, headerId);
   5558                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
   5559                 context.startActivity(intent);
   5560                 return 1;
   5561             }
   5562             return 0;
   5563         } finally {
   5564             ac.close();
   5565         }
   5566     }
   5567 
   5568     private int pickTrashFolder(Uri uri) {
   5569         return pickFolder(uri, Mailbox.TYPE_TRASH, R.string.trash_folder_selection_title);
   5570     }
   5571 
   5572     private int pickSentFolder(Uri uri) {
   5573         return pickFolder(uri, Mailbox.TYPE_SENT, R.string.sent_folder_selection_title);
   5574     }
   5575 
   5576     private Cursor uiUndo(String[] projection) {
   5577         // First see if we have any operations saved
   5578         // TODO: Make sure seq matches
   5579         if (!mLastSequenceOps.isEmpty()) {
   5580             try {
   5581                 // TODO Always use this projection?  Or what's passed in?
   5582                 // Not sure if UI wants it, but I'm making a cursor of convo uri's
   5583                 MatrixCursor c = new MatrixCursorWithCachedColumns(
   5584                         new String[] {UIProvider.ConversationColumns.URI},
   5585                         mLastSequenceOps.size());
   5586                 for (ContentProviderOperation op: mLastSequenceOps) {
   5587                     c.addRow(new String[] {op.getUri().toString()});
   5588                 }
   5589                 // Just apply the batch and we're done!
   5590                 applyBatch(mLastSequenceOps);
   5591                 // But clear the operations
   5592                 mLastSequenceOps.clear();
   5593                 return c;
   5594             } catch (OperationApplicationException e) {
   5595                 LogUtils.d(TAG, "applyBatch exception");
   5596             }
   5597         }
   5598         return new MatrixCursorWithCachedColumns(projection, 0);
   5599     }
   5600 
   5601     private void notifyUIConversation(Uri uri) {
   5602         String id = uri.getLastPathSegment();
   5603         Message msg = Message.restoreMessageWithId(getContext(), Long.parseLong(id));
   5604         if (msg != null) {
   5605             notifyUIConversationMailbox(msg.mMailboxKey);
   5606         }
   5607     }
   5608 
   5609     /**
   5610      * Notify about the Mailbox id passed in
   5611      * @param id the Mailbox id to be notified
   5612      */
   5613     private void notifyUIConversationMailbox(long id) {
   5614         notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, Long.toString(id));
   5615         Mailbox mailbox = Mailbox.restoreMailboxWithId(getContext(), id);
   5616         if (mailbox == null) {
   5617             LogUtils.w(TAG, "No mailbox for notification: " + id);
   5618             return;
   5619         }
   5620         // Notify combined inbox...
   5621         if (mailbox.mType == Mailbox.TYPE_INBOX) {
   5622             notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER,
   5623                     EmailProvider.combinedMailboxId(Mailbox.TYPE_INBOX));
   5624         }
   5625         notifyWidgets(id);
   5626     }
   5627 
   5628     /**
   5629      * Notify about the message id passed in
   5630      * @param id the message id to be notified
   5631      */
   5632     private void notifyUIMessage(long id) {
   5633         notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, id);
   5634     }
   5635 
   5636     /**
   5637      * Notify about the message id passed in
   5638      * @param id the message id to be notified
   5639      */
   5640     private void notifyUIMessage(String id) {
   5641         notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, id);
   5642     }
   5643 
   5644     /**
   5645      * Notify about the Account id passed in
   5646      * @param id the Account id to be notified
   5647      */
   5648     private void notifyUIAccount(long id) {
   5649         // Notify on the specific account
   5650         notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, Long.toString(id));
   5651 
   5652         // Notify on the all accounts list
   5653         notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null);
   5654     }
   5655 
   5656     // TODO: temporary workaround for ConversationCursor
   5657     @Deprecated
   5658     private static final int NOTIFY_FOLDER_LOOP_MESSAGE_ID = 0;
   5659     @Deprecated
   5660     private Handler mFolderNotifierHandler;
   5661 
   5662     /**
   5663      * Notify about a folder update. Because folder changes can affect the conversation cursor's
   5664      * extras, the conversation must also be notified here.
   5665      * @param folderId the folder id to be notified
   5666      * @param accountId the account id to be notified (for folder list notification).
   5667      */
   5668     private void notifyUIFolder(final String folderId, final long accountId) {
   5669         notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, folderId);
   5670         notifyUI(UIPROVIDER_FOLDER_NOTIFIER, folderId);
   5671         if (accountId != Account.NO_ACCOUNT) {
   5672             notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId);
   5673         }
   5674 
   5675         // Notify for combined account too
   5676         // TODO: might be nice to only notify when an inbox changes
   5677         notifyUI(UIPROVIDER_FOLDER_NOTIFIER,
   5678                 getVirtualMailboxId(COMBINED_ACCOUNT_ID, Mailbox.TYPE_INBOX));
   5679         notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, COMBINED_ACCOUNT_ID);
   5680 
   5681         // TODO: temporary workaround for ConversationCursor
   5682         synchronized (this) {
   5683             if (mFolderNotifierHandler == null) {
   5684                 mFolderNotifierHandler = new Handler(Looper.getMainLooper(),
   5685                         new Callback() {
   5686                             @Override
   5687                             public boolean handleMessage(final android.os.Message message) {
   5688                                 final String folderId = (String) message.obj;
   5689                                 LogUtils.d(TAG, "Notifying conversation Uri %s twice", folderId);
   5690                                 notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, folderId);
   5691                                 return true;
   5692                             }
   5693                         });
   5694             }
   5695         }
   5696         mFolderNotifierHandler.removeMessages(NOTIFY_FOLDER_LOOP_MESSAGE_ID);
   5697         android.os.Message message = android.os.Message.obtain(mFolderNotifierHandler,
   5698                 NOTIFY_FOLDER_LOOP_MESSAGE_ID);
   5699         message.obj = folderId;
   5700         mFolderNotifierHandler.sendMessageDelayed(message, 2000);
   5701     }
   5702 
   5703     private void notifyUIFolder(final long folderId, final long accountId) {
   5704         notifyUIFolder(Long.toString(folderId), accountId);
   5705     }
   5706 
   5707     private void notifyUI(final Uri uri, final String id) {
   5708         final Uri notifyUri = (id != null) ? uri.buildUpon().appendPath(id).build() : uri;
   5709         final Set<Uri> batchNotifications = getBatchNotificationsSet();
   5710         if (batchNotifications != null) {
   5711             batchNotifications.add(notifyUri);
   5712         } else {
   5713             getContext().getContentResolver().notifyChange(notifyUri, null);
   5714         }
   5715     }
   5716 
   5717     private void notifyUI(Uri uri, long id) {
   5718         notifyUI(uri, Long.toString(id));
   5719     }
   5720 
   5721     private Mailbox getMailbox(final Uri uri) {
   5722         final long id = Long.parseLong(uri.getLastPathSegment());
   5723         return Mailbox.restoreMailboxWithId(getContext(), id);
   5724     }
   5725 
   5726     /**
   5727      * Create an android.accounts.Account object for this account.
   5728      * @param accountId id of account to load.
   5729      * @return an android.accounts.Account for this account, or null if we can't load it.
   5730      */
   5731     private android.accounts.Account getAccountManagerAccount(final long accountId) {
   5732         final Context context = getContext();
   5733         final Account account = Account.restoreAccountWithId(context, accountId);
   5734         if (account == null) return null;
   5735         return getAccountManagerAccount(context, account.mEmailAddress,
   5736                 account.getProtocol(context));
   5737     }
   5738 
   5739     /**
   5740      * Create an android.accounts.Account object for an emailAddress/protocol pair.
   5741      * @param context A {@link Context}.
   5742      * @param emailAddress The email address we're interested in.
   5743      * @param protocol The protocol we're intereted in.
   5744      * @return an {@link android.accounts.Account} for this info.
   5745      */
   5746     private static android.accounts.Account getAccountManagerAccount(final Context context,
   5747             final String emailAddress, final String protocol) {
   5748         final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol);
   5749         if (info == null) {
   5750             return null;
   5751         }
   5752         return new android.accounts.Account(emailAddress, info.accountType);
   5753     }
   5754 
   5755     /**
   5756      * Update an account's periodic sync if the sync interval has changed.
   5757      * @param accountId id for the account to update.
   5758      * @param values the ContentValues for this update to the account.
   5759      */
   5760     private void updateAccountSyncInterval(final long accountId, final ContentValues values) {
   5761         final Integer syncInterval = values.getAsInteger(AccountColumns.SYNC_INTERVAL);
   5762         if (syncInterval == null) {
   5763             // No change to the sync interval.
   5764             return;
   5765         }
   5766         final android.accounts.Account account = getAccountManagerAccount(accountId);
   5767         if (account == null) {
   5768             // Unable to load the account, or unknown protocol.
   5769             return;
   5770         }
   5771 
   5772         LogUtils.d(TAG, "Setting sync interval for account %s to %d minutes",
   5773                 accountId, syncInterval);
   5774 
   5775         // First remove all existing periodic syncs.
   5776         final List<PeriodicSync> syncs =
   5777                 ContentResolver.getPeriodicSyncs(account, EmailContent.AUTHORITY);
   5778         for (final PeriodicSync sync : syncs) {
   5779             ContentResolver.removePeriodicSync(account, EmailContent.AUTHORITY, sync.extras);
   5780         }
   5781 
   5782         // Only positive values of sync interval indicate periodic syncs. The value is in minutes,
   5783         // while addPeriodicSync expects its time in seconds.
   5784         if (syncInterval > 0) {
   5785             ContentResolver.addPeriodicSync(account, EmailContent.AUTHORITY, Bundle.EMPTY,
   5786                     syncInterval * DateUtils.MINUTE_IN_MILLIS / DateUtils.SECOND_IN_MILLIS);
   5787         }
   5788     }
   5789 
   5790     /**
   5791      * Request a sync.
   5792      * @param account The {@link android.accounts.Account} we want to sync.
   5793      * @param mailboxId The mailbox id we want to sync (or one of the special constants in
   5794      *                  {@link Mailbox}).
   5795      * @param deltaMessageCount If we're requesting a load more, the number of additional messages
   5796      *                          to sync.
   5797      */
   5798     private static void startSync(final android.accounts.Account account, final long mailboxId,
   5799             final int deltaMessageCount) {
   5800         final Bundle extras = Mailbox.createSyncBundle(mailboxId);
   5801         extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
   5802         extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true);
   5803         extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
   5804         if (deltaMessageCount != 0) {
   5805             extras.putInt(Mailbox.SYNC_EXTRA_DELTA_MESSAGE_COUNT, deltaMessageCount);
   5806         }
   5807         extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_URI,
   5808                 EmailContent.CONTENT_URI.toString());
   5809         extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD,
   5810                 SYNC_STATUS_CALLBACK_METHOD);
   5811         ContentResolver.requestSync(account, EmailContent.AUTHORITY, extras);
   5812         LogUtils.i(TAG, "requestSync EmailProvider startSync %s, %s", account.toString(),
   5813                 extras.toString());
   5814     }
   5815 
   5816     /**
   5817      * Request a sync.
   5818      * @param mailbox The {@link Mailbox} we want to sync.
   5819      * @param deltaMessageCount If we're requesting a load more, the number of additional messages
   5820      *                          to sync.
   5821      */
   5822     private void startSync(final Mailbox mailbox, final int deltaMessageCount) {
   5823         final android.accounts.Account account = getAccountManagerAccount(mailbox.mAccountKey);
   5824         if (account != null) {
   5825             startSync(account, mailbox.mId, deltaMessageCount);
   5826         }
   5827     }
   5828 
   5829     /**
   5830      * Restart any push operations for an account.
   5831      * @param account The {@link android.accounts.Account} we're interested in.
   5832      */
   5833     private static void restartPush(final android.accounts.Account account) {
   5834         final Bundle extras = new Bundle();
   5835         extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
   5836         extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true);
   5837         extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
   5838         extras.putBoolean(Mailbox.SYNC_EXTRA_PUSH_ONLY, true);
   5839         extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_URI,
   5840                 EmailContent.CONTENT_URI.toString());
   5841         extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD,
   5842                 SYNC_STATUS_CALLBACK_METHOD);
   5843         ContentResolver.requestSync(account, EmailContent.AUTHORITY, extras);
   5844         LogUtils.i(TAG, "requestSync EmailProvider startSync %s, %s", account.toString(),
   5845                 extras.toString());
   5846     }
   5847 
   5848     private Cursor uiFolderRefresh(final Mailbox mailbox, final int deltaMessageCount) {
   5849         if (mailbox != null) {
   5850             RefreshStatusMonitor.getInstance(getContext())
   5851                     .monitorRefreshStatus(mailbox.mId, new RefreshStatusMonitor.Callback() {
   5852                 @Override
   5853                 public void onRefreshCompleted(long mailboxId, int result) {
   5854                     // all calls to this method assumed to be started by a user action
   5855                     final int syncValue = UIProvider.createSyncValue(EmailContent.SYNC_STATUS_USER,
   5856                             result);
   5857                     final ContentValues values = new ContentValues();
   5858                     values.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC);
   5859                     values.put(Mailbox.UI_LAST_SYNC_RESULT, syncValue);
   5860                     mDatabase.update(Mailbox.TABLE_NAME, values, WHERE_ID,
   5861                             new String[] { String.valueOf(mailboxId) });
   5862                     notifyUIFolder(mailbox.mId, mailbox.mAccountKey);
   5863                 }
   5864 
   5865                 @Override
   5866                 public void onTimeout(long mailboxId) {
   5867                     // todo
   5868                 }
   5869             });
   5870             startSync(mailbox, deltaMessageCount);
   5871         }
   5872         return null;
   5873     }
   5874 
   5875     //Number of additional messages to load when a user selects "Load more..." in POP/IMAP boxes
   5876     public static final int VISIBLE_LIMIT_INCREMENT = 10;
   5877     //Number of additional messages to load when a user selects "Load more..." in a search
   5878     public static final int SEARCH_MORE_INCREMENT = 10;
   5879 
   5880     private Cursor uiFolderLoadMore(final Mailbox mailbox) {
   5881         if (mailbox == null) return null;
   5882         if (mailbox.mType == Mailbox.TYPE_SEARCH) {
   5883             // Ask for 10 more messages
   5884             mSearchParams.mOffset += SEARCH_MORE_INCREMENT;
   5885             runSearchQuery(getContext(), mailbox.mAccountKey, mailbox.mId);
   5886         } else {
   5887             uiFolderRefresh(mailbox, VISIBLE_LIMIT_INCREMENT);
   5888         }
   5889         return null;
   5890     }
   5891 
   5892     private static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__";
   5893     private SearchParams mSearchParams;
   5894 
   5895     /**
   5896      * Returns the search mailbox for the specified account, creating one if necessary
   5897      * @return the search mailbox for the passed in account
   5898      */
   5899     private Mailbox getSearchMailbox(long accountId) {
   5900         Context context = getContext();
   5901         Mailbox m = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SEARCH);
   5902         if (m == null) {
   5903             m = new Mailbox();
   5904             m.mAccountKey = accountId;
   5905             m.mServerId = SEARCH_MAILBOX_SERVER_ID;
   5906             m.mFlagVisible = false;
   5907             m.mDisplayName = SEARCH_MAILBOX_SERVER_ID;
   5908             m.mSyncInterval = 0;
   5909             m.mType = Mailbox.TYPE_SEARCH;
   5910             m.mFlags = Mailbox.FLAG_HOLDS_MAIL;
   5911             m.mParentKey = Mailbox.NO_MAILBOX;
   5912             m.save(context);
   5913         }
   5914         return m;
   5915     }
   5916 
   5917     private void runSearchQuery(final Context context, final long accountId,
   5918             final long searchMailboxId) {
   5919         LogUtils.d(TAG, "runSearchQuery. account: %d mailbox id: %d",
   5920                 accountId, searchMailboxId);
   5921 
   5922         // Start the search running in the background
   5923         new AsyncTask<Void, Void, Void>() {
   5924             @Override
   5925             public Void doInBackground(Void... params) {
   5926                 final EmailServiceProxy service =
   5927                         EmailServiceUtils.getServiceForAccount(context, accountId);
   5928                 if (service != null) {
   5929                     try {
   5930                         final int totalCount =
   5931                                 service.searchMessages(accountId, mSearchParams, searchMailboxId);
   5932 
   5933                         // Save away the total count
   5934                         final ContentValues cv = new ContentValues(1);
   5935                         cv.put(MailboxColumns.TOTAL_COUNT, totalCount);
   5936                         update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId), cv,
   5937                                 null, null);
   5938                         LogUtils.d(TAG, "EmailProvider#runSearchQuery. TotalCount to UI: %d",
   5939                                 totalCount);
   5940                     } catch (RemoteException e) {
   5941                         LogUtils.e("searchMessages", "RemoteException", e);
   5942                     }
   5943                 }
   5944                 return null;
   5945             }
   5946         }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
   5947     }
   5948 
   5949     // This handles an initial search query. More results are loaded using uiFolderLoadMore.
   5950     private Cursor uiSearch(Uri uri, String[] projection) {
   5951         LogUtils.d(TAG, "runSearchQuery in search %s", uri);
   5952         final long accountId = Long.parseLong(uri.getLastPathSegment());
   5953 
   5954         // TODO: Check the actual mailbox
   5955         Mailbox inbox = Mailbox.restoreMailboxOfType(getContext(), accountId, Mailbox.TYPE_INBOX);
   5956         if (inbox == null) {
   5957             LogUtils.w(Logging.LOG_TAG, "In uiSearch, inbox doesn't exist for account "
   5958                     + accountId);
   5959 
   5960             return null;
   5961         }
   5962 
   5963         String filter = uri.getQueryParameter(UIProvider.SearchQueryParameters.QUERY);
   5964         if (filter == null) {
   5965             throw new IllegalArgumentException("No query parameter in search query");
   5966         }
   5967 
   5968         // Find/create our search mailbox
   5969         Mailbox searchMailbox = getSearchMailbox(accountId);
   5970         final long searchMailboxId = searchMailbox.mId;
   5971 
   5972         mSearchParams = new SearchParams(inbox.mId, filter, searchMailboxId);
   5973 
   5974         final Context context = getContext();
   5975         if (mSearchParams.mOffset == 0) {
   5976             // TODO: This conditional is unnecessary, just two lines earlier we created
   5977             // mSearchParams using a constructor that never sets mOffset.
   5978             LogUtils.d(TAG, "deleting existing search results.");
   5979             final ContentResolver resolver = context.getContentResolver();
   5980             final ContentValues cv = new ContentValues(3);
   5981             // For now, use the actual query as the name of the mailbox
   5982             cv.put(Mailbox.DISPLAY_NAME, mSearchParams.mFilter);
   5983             // We are about to do a sync on this folder, but if the UI is refreshed before the
   5984             // service can start its query, we need it to see that there is a sync in progress.
   5985             // Otherwise it could show the empty state, until the service gets around to setting
   5986             // the syncState.
   5987             cv.put(Mailbox.UI_SYNC_STATUS, EmailContent.SYNC_STATUS_LIVE);
   5988             // We don't know how many result we'll have yet, but we assume zero until we get
   5989             // a response back from the server. Otherwise, we'll whatever count there was on the
   5990             // previous search, and we'll display the "Load More" footer prior to having
   5991             // any results.
   5992             cv.put(Mailbox.TOTAL_COUNT, 0);
   5993             resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId),
   5994                     cv, null, null);
   5995 
   5996             // Delete existing contents of search mailbox
   5997             resolver.delete(Message.CONTENT_URI, MessageColumns.MAILBOX_KEY + "=" + searchMailboxId,
   5998                     null);
   5999         }
   6000 
   6001         // Start the search running in the background
   6002         runSearchQuery(context, accountId, searchMailboxId);
   6003 
   6004         // This will look just like a "normal" folder
   6005         return uiQuery(UI_FOLDER, ContentUris.withAppendedId(Mailbox.CONTENT_URI,
   6006                 searchMailbox.mId), projection, false);
   6007     }
   6008 
   6009     private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?";
   6010 
   6011     /**
   6012      * Delete an account and clean it up
   6013      */
   6014     private int uiDeleteAccount(Uri uri) {
   6015         Context context = getContext();
   6016         long accountId = Long.parseLong(uri.getLastPathSegment());
   6017         try {
   6018             // Get the account URI.
   6019             final Account account = Account.restoreAccountWithId(context, accountId);
   6020             if (account == null) {
   6021                 return 0; // Already deleted?
   6022             }
   6023 
   6024             deleteAccountData(context, accountId);
   6025 
   6026             // Now delete the account itself
   6027             uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
   6028             context.getContentResolver().delete(uri, null, null);
   6029 
   6030             // Clean up
   6031             AccountBackupRestore.backup(context);
   6032             SecurityPolicy.getInstance(context).reducePolicies();
   6033             setServicesEnabledSync(context);
   6034             // TODO: We ought to reconcile accounts here, but some callers do this in a loop,
   6035             // which would be a problem when the first account reconciliation shuts us down.
   6036             return 1;
   6037         } catch (Exception e) {
   6038             LogUtils.w(Logging.LOG_TAG, "Exception while deleting account", e);
   6039         }
   6040         return 0;
   6041     }
   6042 
   6043     private int uiDeleteAccountData(Uri uri) {
   6044         Context context = getContext();
   6045         long accountId = Long.parseLong(uri.getLastPathSegment());
   6046         // Get the account URI.
   6047         final Account account = Account.restoreAccountWithId(context, accountId);
   6048         if (account == null) {
   6049             return 0; // Already deleted?
   6050         }
   6051         deleteAccountData(context, accountId);
   6052         return 1;
   6053     }
   6054 
   6055     /**
   6056      * The method will no longer be needed after platform L releases. As emails are received from
   6057      * various protocols the email addresses are decoded and intended to be stored in the database
   6058      * in decoded form. The problem is that Exchange is a separate .apk and the old Exchange .apk
   6059      * still attempts to store <strong>encoded</strong> email addresses. So, we decode here at the
   6060      * Provider before writing to the database to ensure the addresses are written in decoded form.
   6061      *
   6062      * @param values the values to be written into the Message table
   6063      */
   6064     private static void decodeEmailAddresses(ContentValues values) {
   6065         if (values.containsKey(Message.MessageColumns.TO_LIST)) {
   6066             final String to = values.getAsString(Message.MessageColumns.TO_LIST);
   6067             values.put(Message.MessageColumns.TO_LIST, Address.fromHeaderToString(to));
   6068         }
   6069 
   6070         if (values.containsKey(Message.MessageColumns.FROM_LIST)) {
   6071             final String from = values.getAsString(Message.MessageColumns.FROM_LIST);
   6072             values.put(Message.MessageColumns.FROM_LIST, Address.fromHeaderToString(from));
   6073         }
   6074 
   6075         if (values.containsKey(Message.MessageColumns.CC_LIST)) {
   6076             final String cc = values.getAsString(Message.MessageColumns.CC_LIST);
   6077             values.put(Message.MessageColumns.CC_LIST, Address.fromHeaderToString(cc));
   6078         }
   6079 
   6080         if (values.containsKey(Message.MessageColumns.BCC_LIST)) {
   6081             final String bcc = values.getAsString(Message.MessageColumns.BCC_LIST);
   6082             values.put(Message.MessageColumns.BCC_LIST, Address.fromHeaderToString(bcc));
   6083         }
   6084 
   6085         if (values.containsKey(Message.MessageColumns.REPLY_TO_LIST)) {
   6086             final String replyTo = values.getAsString(Message.MessageColumns.REPLY_TO_LIST);
   6087             values.put(Message.MessageColumns.REPLY_TO_LIST,
   6088                     Address.fromHeaderToString(replyTo));
   6089         }
   6090     }
   6091 
   6092     /** Projection used for getting email address for an account. */
   6093     private static final String[] ACCOUNT_EMAIL_PROJECTION = { AccountColumns.EMAIL_ADDRESS };
   6094 
   6095     private static void deleteAccountData(Context context, long accountId) {
   6096         // We will delete PIM data, but by the time the asynchronous call to do that happens,
   6097         // the account may have been deleted from the DB. Therefore we have to get the email
   6098         // address now and send that, rather than the account id.
   6099         final String emailAddress = Utility.getFirstRowString(context, Account.CONTENT_URI,
   6100                 ACCOUNT_EMAIL_PROJECTION, Account.ID_SELECTION,
   6101                 new String[] {Long.toString(accountId)}, null, 0);
   6102         if (emailAddress == null) {
   6103             LogUtils.e(TAG, "Could not find email address for account %d", accountId);
   6104         }
   6105 
   6106         // Delete synced attachments
   6107         AttachmentUtilities.deleteAllAccountAttachmentFiles(context, accountId);
   6108 
   6109         // Delete all mailboxes.
   6110         ContentResolver resolver = context.getContentResolver();
   6111         String[] accountIdArgs = new String[] { Long.toString(accountId) };
   6112         resolver.delete(Mailbox.CONTENT_URI, MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs);
   6113 
   6114         // Delete account sync key.
   6115         final ContentValues cv = new ContentValues();
   6116         cv.putNull(AccountColumns.SYNC_KEY);
   6117         resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs);
   6118 
   6119         // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable
   6120         if (emailAddress != null) {
   6121             final IEmailService service =
   6122                     EmailServiceUtils.getServiceForAccount(context, accountId);
   6123             if (service != null) {
   6124                 try {
   6125                     service.deleteExternalAccountPIMData(emailAddress);
   6126                 } catch (final RemoteException e) {
   6127                     // Can't do anything about this
   6128                 }
   6129             }
   6130         }
   6131     }
   6132 
   6133     private int[] mSavedWidgetIds = new int[0];
   6134     private final ArrayList<Long> mWidgetNotifyMailboxes = new ArrayList<Long>();
   6135     private AppWidgetManager mAppWidgetManager;
   6136     private ComponentName mEmailComponent;
   6137 
   6138     private void notifyWidgets(long mailboxId) {
   6139         Context context = getContext();
   6140         // Lazily initialize these
   6141         if (mAppWidgetManager == null) {
   6142             mAppWidgetManager = AppWidgetManager.getInstance(context);
   6143             mEmailComponent = new ComponentName(context, WidgetProvider.getProviderName(context));
   6144         }
   6145 
   6146         // See if we have to populate our array of mailboxes used in widgets
   6147         int[] widgetIds = mAppWidgetManager.getAppWidgetIds(mEmailComponent);
   6148         if (!Arrays.equals(widgetIds, mSavedWidgetIds)) {
   6149             mSavedWidgetIds = widgetIds;
   6150             String[][] widgetInfos = BaseWidgetProvider.getWidgetInfo(context, widgetIds);
   6151             // widgetInfo now has pairs of account uri/folder uri
   6152             mWidgetNotifyMailboxes.clear();
   6153             for (String[] widgetInfo: widgetInfos) {
   6154                 try {
   6155                     if (widgetInfo == null || TextUtils.isEmpty(widgetInfo[1])) continue;
   6156                     long id = Long.parseLong(Uri.parse(widgetInfo[1]).getLastPathSegment());
   6157                     if (!isCombinedMailbox(id)) {
   6158                         // For a regular mailbox, just add it to the list
   6159                         if (!mWidgetNotifyMailboxes.contains(id)) {
   6160                             mWidgetNotifyMailboxes.add(id);
   6161                         }
   6162                     } else {
   6163                         switch (getVirtualMailboxType(id)) {
   6164                             // We only handle the combined inbox in widgets
   6165                             case Mailbox.TYPE_INBOX:
   6166                                 Cursor c = query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION,
   6167                                         MailboxColumns.TYPE + "=?",
   6168                                         new String[] {Integer.toString(Mailbox.TYPE_INBOX)}, null);
   6169                                 try {
   6170                                     while (c.moveToNext()) {
   6171                                         mWidgetNotifyMailboxes.add(
   6172                                                 c.getLong(Mailbox.ID_PROJECTION_COLUMN));
   6173                                     }
   6174                                 } finally {
   6175                                     c.close();
   6176                                 }
   6177                                 break;
   6178                         }
   6179                     }
   6180                 } catch (NumberFormatException e) {
   6181                     // Move along
   6182                 }
   6183             }
   6184         }
   6185 
   6186         // If our mailbox needs to be notified, do so...
   6187         if (mWidgetNotifyMailboxes.contains(mailboxId)) {
   6188             Intent intent = new Intent(Utils.ACTION_NOTIFY_DATASET_CHANGED);
   6189             intent.putExtra(Utils.EXTRA_FOLDER_URI, uiUri("uifolder", mailboxId));
   6190             intent.setType(EMAIL_APP_MIME_TYPE);
   6191             context.sendBroadcast(intent);
   6192          }
   6193     }
   6194 
   6195     @Override
   6196     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
   6197         Context context = getContext();
   6198         writer.println("Installed services:");
   6199         for (EmailServiceInfo info: EmailServiceUtils.getServiceInfoList(context)) {
   6200             writer.println("  " + info);
   6201         }
   6202         writer.println();
   6203         writer.println("Accounts: ");
   6204         Cursor cursor = query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null, null);
   6205         if (cursor.getCount() == 0) {
   6206             writer.println("  None");
   6207         }
   6208         try {
   6209             while (cursor.moveToNext()) {
   6210                 Account account = new Account();
   6211                 account.restore(cursor);
   6212                 writer.println("  Account " + account.mDisplayName);
   6213                 HostAuth hostAuth =
   6214                         HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
   6215                 if (hostAuth != null) {
   6216                     writer.println("    Protocol = " + hostAuth.mProtocol +
   6217                             (TextUtils.isEmpty(account.mProtocolVersion) ? "" : " version " +
   6218                                     account.mProtocolVersion));
   6219                 }
   6220             }
   6221         } finally {
   6222             cursor.close();
   6223         }
   6224     }
   6225 
   6226     synchronized public Handler getDelayedSyncHandler() {
   6227         if (mDelayedSyncHandler == null) {
   6228             mDelayedSyncHandler = new Handler(getContext().getMainLooper(), new Callback() {
   6229                 @Override
   6230                 public boolean handleMessage(android.os.Message msg) {
   6231                     synchronized (mDelayedSyncRequests) {
   6232                         final SyncRequestMessage request = (SyncRequestMessage) msg.obj;
   6233                         // TODO: It's possible that the account is deleted by the time we get here
   6234                         // It would be nice if we could validate it before trying to sync
   6235                         final android.accounts.Account account = request.mAccount;
   6236                         final Bundle extras = Mailbox.createSyncBundle(request.mMailboxId);
   6237                         ContentResolver.requestSync(account, request.mAuthority, extras);
   6238                         LogUtils.i(TAG, "requestSync getDelayedSyncHandler %s, %s",
   6239                                 account.toString(), extras.toString());
   6240                         mDelayedSyncRequests.remove(request);
   6241                         return true;
   6242                     }
   6243                 }
   6244             });
   6245         }
   6246         return mDelayedSyncHandler;
   6247     }
   6248 
   6249     private class SyncRequestMessage {
   6250         private final String mAuthority;
   6251         private final android.accounts.Account mAccount;
   6252         private final long mMailboxId;
   6253 
   6254         private SyncRequestMessage(final String authority, final android.accounts.Account account,
   6255                 final long mailboxId) {
   6256             mAuthority = authority;
   6257             mAccount = account;
   6258             mMailboxId = mailboxId;
   6259         }
   6260 
   6261         @Override
   6262         public boolean equals(Object o) {
   6263             if (this == o) {
   6264                 return true;
   6265             }
   6266             if (o == null || getClass() != o.getClass()) {
   6267                 return false;
   6268             }
   6269 
   6270             SyncRequestMessage that = (SyncRequestMessage) o;
   6271 
   6272             return mAccount.equals(that.mAccount)
   6273                     && mMailboxId == that.mMailboxId
   6274                     && mAuthority.equals(that.mAuthority);
   6275         }
   6276 
   6277         @Override
   6278         public int hashCode() {
   6279             int result = mAuthority.hashCode();
   6280             result = 31 * result + mAccount.hashCode();
   6281             result = 31 * result + (int) (mMailboxId ^ (mMailboxId >>> 32));
   6282             return result;
   6283         }
   6284     }
   6285 
   6286     @Override
   6287     public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
   6288         if (PreferenceKeys.REMOVAL_ACTION.equals(key) ||
   6289                 PreferenceKeys.CONVERSATION_LIST_SWIPE.equals(key) ||
   6290                 PreferenceKeys.SHOW_SENDER_IMAGES.equals(key) ||
   6291                 PreferenceKeys.DEFAULT_REPLY_ALL.equals(key) ||
   6292                 PreferenceKeys.CONVERSATION_OVERVIEW_MODE.equals(key) ||
   6293                 PreferenceKeys.AUTO_ADVANCE_MODE.equals(key) ||
   6294                 PreferenceKeys.SNAP_HEADER_MODE.equals(key) ||
   6295                 PreferenceKeys.CONFIRM_DELETE.equals(key) ||
   6296                 PreferenceKeys.CONFIRM_ARCHIVE.equals(key) ||
   6297                 PreferenceKeys.CONFIRM_SEND.equals(key)) {
   6298             notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null);
   6299         }
   6300     }
   6301 
   6302     /**
   6303      * Asynchronous version of {@link #setServicesEnabledSync(Context)}.  Use when calling from
   6304      * UI thread (or lifecycle entry points.)
   6305      */
   6306     public static void setServicesEnabledAsync(final Context context) {
   6307         if (context.getResources().getBoolean(R.bool.enable_services)) {
   6308             EmailAsyncTask.runAsyncParallel(new Runnable() {
   6309                 @Override
   6310                 public void run() {
   6311                     setServicesEnabledSync(context);
   6312                 }
   6313             });
   6314         }
   6315     }
   6316 
   6317     /**
   6318      * Called throughout the application when the number of accounts has changed. This method
   6319      * enables or disables the Compose activity, the boot receiver and the service based on
   6320      * whether any accounts are configured.
   6321      *
   6322      * Blocking call - do not call from UI/lifecycle threads.
   6323      *
   6324      * @return true if there are any accounts configured.
   6325      */
   6326     public static boolean setServicesEnabledSync(Context context) {
   6327         // Make sure we're initialized
   6328         EmailContent.init(context);
   6329         Cursor c = null;
   6330         try {
   6331             c = context.getContentResolver().query(
   6332                     Account.CONTENT_URI,
   6333                     Account.ID_PROJECTION,
   6334                     null, null, null);
   6335             boolean enable = c != null && c.getCount() > 0;
   6336             setServicesEnabled(context, enable);
   6337             return enable;
   6338         } finally {
   6339             if (c != null) {
   6340                 c.close();
   6341             }
   6342         }
   6343     }
   6344 
   6345     private static void setServicesEnabled(Context context, boolean enabled) {
   6346         PackageManager pm = context.getPackageManager();
   6347         pm.setComponentEnabledSetting(
   6348                 new ComponentName(context, AttachmentService.class),
   6349                 enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
   6350                         PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
   6351                 PackageManager.DONT_KILL_APP);
   6352 
   6353         // Start/stop the various services depending on whether there are any accounts
   6354         // TODO: Make sure that the AttachmentService responds to this request as it
   6355         // expects a particular set of data in the intents that it receives or it ignores.
   6356         startOrStopService(enabled, context, new Intent(context, AttachmentService.class));
   6357         final NotificationController controller =
   6358                 NotificationControllerCreatorHolder.getInstance(context);
   6359 
   6360         if (controller != null) {
   6361             controller.watchForMessages();
   6362         }
   6363     }
   6364 
   6365     /**
   6366      * Starts or stops the service as necessary.
   6367      * @param enabled If {@code true}, the service will be started. Otherwise, it will be stopped.
   6368      * @param context The context to manage the service with.
   6369      * @param intent The intent of the service to be managed.
   6370      */
   6371     private static void startOrStopService(boolean enabled, Context context, Intent intent) {
   6372         if (enabled) {
   6373             context.startService(intent);
   6374         } else {
   6375             context.stopService(intent);
   6376         }
   6377     }
   6378 
   6379 
   6380     public static Uri getIncomingSettingsUri(long accountId) {
   6381         final Uri.Builder baseUri = Uri.parse("auth://" + EmailContent.EMAIL_PACKAGE_NAME +
   6382                 ".ACCOUNT_SETTINGS/incoming/").buildUpon();
   6383         IntentUtilities.setAccountId(baseUri, accountId);
   6384         return baseUri.build();
   6385     }
   6386 
   6387 }
   6388