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.emailcommon.provider;
     18 
     19 import android.content.ContentProviderOperation;
     20 import android.content.ContentProviderResult;
     21 import android.content.ContentResolver;
     22 import android.content.ContentUris;
     23 import android.content.ContentValues;
     24 import android.content.Context;
     25 import android.content.OperationApplicationException;
     26 import android.content.res.Resources;
     27 import android.database.ContentObservable;
     28 import android.database.ContentObserver;
     29 import android.database.Cursor;
     30 import android.net.Uri;
     31 import android.os.Environment;
     32 import android.os.Looper;
     33 import android.os.Parcel;
     34 import android.os.Parcelable;
     35 import android.os.RemoteException;
     36 import android.provider.BaseColumns;
     37 
     38 import com.android.emailcommon.Logging;
     39 import com.android.emailcommon.R;
     40 import com.android.emailcommon.utility.TextUtilities;
     41 import com.android.emailcommon.utility.Utility;
     42 import com.android.mail.providers.UIProvider;
     43 import com.android.mail.utils.LogUtils;
     44 import com.google.common.annotations.VisibleForTesting;
     45 
     46 import org.apache.commons.io.IOUtils;
     47 
     48 import java.io.File;
     49 import java.io.IOException;
     50 import java.io.InputStream;
     51 import java.lang.ref.WeakReference;
     52 import java.util.ArrayList;
     53 
     54 
     55 /**
     56  * EmailContent is the superclass of the various classes of content stored by EmailProvider.
     57  *
     58  * It is intended to include 1) column definitions for use with the Provider, and 2) convenience
     59  * methods for saving and retrieving content from the Provider.
     60  *
     61  * This class will be used by 1) the Email process (which includes the application and
     62  * EmaiLProvider) as well as 2) the Exchange process (which runs independently).  It will
     63  * necessarily be cloned for use in these two cases.
     64  *
     65  * Conventions used in naming columns:
     66  *   BaseColumns._ID is the primary key for all Email records
     67  *   The SyncColumns interface is used by all classes that are synced to the server directly
     68  *   (Mailbox and Email)
     69  *
     70  *   <name>_KEY always refers to a foreign key
     71  *   <name>_ID always refers to a unique identifier (whether on client, server, etc.)
     72  *
     73  */
     74 public abstract class EmailContent {
     75     public static final int NOTIFICATION_MAILBOX_ID_COLUMN = 0;
     76     public static final int NOTIFICATION_MAILBOX_UNREAD_COUNT_COLUMN = 1;
     77     public static final int NOTIFICATION_MAILBOX_UNSEEN_COUNT_COLUMN = 2;
     78 
     79     // All classes share this
     80     // Use BaseColumns._ID instead
     81     @Deprecated
     82     public static final String RECORD_ID = "_id";
     83 
     84     public static final String[] COUNT_COLUMNS = {"count(*)"};
     85 
     86     /**
     87      * This projection can be used with any of the EmailContent classes, when all you need
     88      * is a list of id's.  Use ID_PROJECTION_COLUMN to access the row data.
     89      */
     90     public static final String[] ID_PROJECTION = { BaseColumns._ID };
     91     public static final int ID_PROJECTION_COLUMN = 0;
     92 
     93     public static final String ID_SELECTION = BaseColumns._ID + " =?";
     94 
     95     public static final int SYNC_STATUS_NONE = UIProvider.SyncStatus.NO_SYNC;
     96     public static final int SYNC_STATUS_USER = UIProvider.SyncStatus.USER_REFRESH;
     97     public static final int SYNC_STATUS_BACKGROUND = UIProvider.SyncStatus.BACKGROUND_SYNC;
     98     public static final int SYNC_STATUS_LIVE = UIProvider.SyncStatus.LIVE_QUERY;
     99     public static final int SYNC_STATUS_INITIAL_SYNC_NEEDED =
    100             UIProvider.SyncStatus.INITIAL_SYNC_NEEDED;
    101 
    102     public static final int LAST_SYNC_RESULT_SUCCESS = UIProvider.LastSyncResult.SUCCESS;
    103     public static final int LAST_SYNC_RESULT_AUTH_ERROR = UIProvider.LastSyncResult.AUTH_ERROR;
    104     public static final int LAST_SYNC_RESULT_SERVER_ERROR = UIProvider.LastSyncResult.SERVER_ERROR;
    105     public static final int LAST_SYNC_RESULT_SECURITY_ERROR =
    106             UIProvider.LastSyncResult.SECURITY_ERROR;
    107     public static final int LAST_SYNC_RESULT_CONNECTION_ERROR =
    108             UIProvider.LastSyncResult.CONNECTION_ERROR;
    109     public static final int LAST_SYNC_RESULT_INTERNAL_ERROR =
    110             UIProvider.LastSyncResult.INTERNAL_ERROR;
    111 
    112     // Newly created objects get this id
    113     public static final int NOT_SAVED = -1;
    114     // The base Uri that this piece of content came from
    115     public Uri mBaseUri;
    116     // Lazily initialized uri for this Content
    117     private Uri mUri = null;
    118     // The id of the Content
    119     public long mId = NOT_SAVED;
    120 
    121     /**
    122      * Since we close the cursor we use to generate this object, and possibly create the object
    123      * without using any cursor at all (eg: parcel), we need to handle observing provider changes
    124      * ourselves. This content observer uses a weak reference to keep from rooting this object
    125      * in the ContentResolver in case it is not properly disposed of using {@link #close(Context)}
    126      */
    127     private SelfContentObserver mSelfObserver;
    128     private ContentObservable mObservable;
    129 
    130     // Write the Content into a ContentValues container
    131     public abstract ContentValues toContentValues();
    132     // Read the Content from a ContentCursor
    133     public abstract void restore(Cursor cursor);
    134     // Same as above, with the addition of a context to retrieve extra content.
    135     // Body uses this to fetch the email body html/text from the provider bypassing the cursor
    136     // Not always safe to call on the UI thread.
    137     public void restore(Context context, Cursor cursor) {
    138         restore(cursor);
    139     }
    140 
    141 
    142     public static String EMAIL_PACKAGE_NAME;
    143     public static String AUTHORITY;
    144     // The notifier authority is used to send notifications regarding changes to messages (insert,
    145     // delete, or update) and is intended as an optimization for use by clients of message list
    146     // cursors (initially, the email AppWidget).
    147     public static String NOTIFIER_AUTHORITY;
    148     public static Uri CONTENT_URI;
    149     public static final String PARAMETER_LIMIT = "limit";
    150 
    151     /**
    152      * Query parameter for the UI accounts query to enable suppression of the combined account.
    153      */
    154     public static final String SUPPRESS_COMBINED_ACCOUNT_PARAM = "suppress_combined";
    155     public static Uri CONTENT_NOTIFIER_URI;
    156     public static Uri PICK_TRASH_FOLDER_URI;
    157     public static Uri PICK_SENT_FOLDER_URI;
    158     public static Uri MAILBOX_NOTIFICATION_URI;
    159     public static Uri MAILBOX_MOST_RECENT_MESSAGE_URI;
    160     public static Uri ACCOUNT_CHECK_URI;
    161 
    162     /**
    163      * String for both the EmailProvider call, and the key for the value in the response.
    164      * TODO: Eventually this ought to be a device property, not defined by the app.
    165      */
    166     public static String DEVICE_FRIENDLY_NAME = "deviceFriendlyName";
    167 
    168 
    169     public static String PROVIDER_PERMISSION;
    170 
    171     public static synchronized void init(Context context) {
    172         if (AUTHORITY == null) {
    173             final Resources res = context.getResources();
    174             EMAIL_PACKAGE_NAME = res.getString(R.string.email_package_name);
    175             AUTHORITY = EMAIL_PACKAGE_NAME + ".provider";
    176             LogUtils.d("EmailContent", "init for " + AUTHORITY);
    177             NOTIFIER_AUTHORITY = EMAIL_PACKAGE_NAME + ".notifier";
    178             CONTENT_URI = Uri.parse("content://" + AUTHORITY);
    179             CONTENT_NOTIFIER_URI = Uri.parse("content://" + NOTIFIER_AUTHORITY);
    180             PICK_TRASH_FOLDER_URI = Uri.parse("content://" + AUTHORITY + "/pickTrashFolder");
    181             PICK_SENT_FOLDER_URI = Uri.parse("content://" + AUTHORITY + "/pickSentFolder");
    182             MAILBOX_NOTIFICATION_URI = Uri.parse("content://" + AUTHORITY + "/mailboxNotification");
    183             MAILBOX_MOST_RECENT_MESSAGE_URI = Uri.parse("content://" + AUTHORITY +
    184                     "/mailboxMostRecentMessage");
    185             ACCOUNT_CHECK_URI = Uri.parse("content://" + AUTHORITY + "/accountCheck");
    186             PROVIDER_PERMISSION = EMAIL_PACKAGE_NAME + ".permission.ACCESS_PROVIDER";
    187             // Initialize subclasses
    188             Account.initAccount();
    189             Mailbox.initMailbox();
    190             QuickResponse.initQuickResponse();
    191             HostAuth.initHostAuth();
    192             Credential.initCredential();
    193             Policy.initPolicy();
    194             Message.initMessage();
    195             MessageMove.init();
    196             MessageStateChange.init();
    197             Body.initBody();
    198             Attachment.initAttachment();
    199         }
    200     }
    201 
    202 
    203     private static void warnIfUiThread() {
    204         if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
    205             LogUtils.w(Logging.LOG_TAG, new Throwable(), "Method called on the UI thread");
    206         }
    207     }
    208 
    209     public static boolean isInitialSyncKey(final String syncKey) {
    210         return syncKey == null || syncKey.isEmpty() || syncKey.equals("0");
    211     }
    212 
    213     // The Uri is lazily initialized
    214     public Uri getUri() {
    215         if (mUri == null) {
    216             mUri = ContentUris.withAppendedId(mBaseUri, mId);
    217         }
    218         return mUri;
    219     }
    220 
    221     public boolean isSaved() {
    222         return mId != NOT_SAVED;
    223     }
    224 
    225 
    226     /**
    227      * Restore a subclass of EmailContent from the database
    228      * @param context the caller's context
    229      * @param klass the class to restore
    230      * @param contentUri the content uri of the EmailContent subclass
    231      * @param contentProjection the content projection for the EmailContent subclass
    232      * @param id the unique id of the object
    233      * @return the instantiated object
    234      */
    235     public static <T extends EmailContent> T restoreContentWithId(Context context,
    236             Class<T> klass, Uri contentUri, String[] contentProjection, long id) {
    237         return restoreContentWithId(context, klass, contentUri, contentProjection, id, null);
    238     }
    239 
    240     public static <T extends EmailContent> T restoreContentWithId(final Context context,
    241                 final Class<T> klass, final Uri contentUri, final String[] contentProjection,
    242             final long id, final ContentObserver observer) {
    243         warnIfUiThread();
    244         final Uri u = ContentUris.withAppendedId(contentUri, id);
    245         final Cursor c = context.getContentResolver().query(u, contentProjection, null, null, null);
    246         if (c == null) throw new ProviderUnavailableException();
    247         try {
    248             if (c.moveToFirst()) {
    249                 final T content = getContent(context, c, klass);
    250                 if (observer != null) {
    251                     content.registerObserver(context, observer);
    252                 }
    253                 return content;
    254             } else {
    255                 return null;
    256             }
    257         } finally {
    258             c.close();
    259         }
    260     }
    261 
    262     /**
    263      * Register a content observer to be notified when the data underlying this object changes
    264      * @param observer ContentObserver to register
    265      */
    266     public synchronized void registerObserver(final Context context, final ContentObserver observer) {
    267         if (mSelfObserver == null) {
    268             mSelfObserver = new SelfContentObserver(this);
    269             context.getContentResolver().registerContentObserver(getContentNotificationUri(),
    270                     true, mSelfObserver);
    271             mObservable = new ContentObservable();
    272         }
    273         mObservable.registerObserver(observer);
    274     }
    275 
    276     /**
    277      * Unregister a content observer previously registered with
    278      * {@link #registerObserver(Context, ContentObserver)}
    279      * @param observer ContentObserver to unregister
    280      */
    281     public synchronized void unregisterObserver(final ContentObserver observer) {
    282         if (mObservable == null) {
    283             throw new IllegalStateException("Unregistering with null observable");
    284         }
    285         mObservable.unregisterObserver(observer);
    286     }
    287 
    288     /**
    289      * Unregister all content observers previously registered with
    290      * {@link #registerObserver(Context, ContentObserver)}
    291      */
    292     public synchronized void unregisterAllObservers() {
    293         if (mObservable == null) {
    294             throw new IllegalStateException("Unregistering with null observable");
    295         }
    296         mObservable.unregisterAll();
    297     }
    298 
    299     /**
    300      * Unregister all content observers previously registered with
    301      * {@link #registerObserver(Context, ContentObserver)} and release internal resources associated
    302      * with content observing
    303      */
    304     public synchronized void close(final Context context) {
    305         if (mSelfObserver == null) {
    306             return;
    307         }
    308         unregisterAllObservers();
    309         context.getContentResolver().unregisterContentObserver(mSelfObserver);
    310         mSelfObserver = null;
    311     }
    312 
    313     /**
    314      * Returns a Uri for observing the underlying content. Subclasses that wish to implement content
    315      * observing must override this method.
    316      * @return Uri for registering content notifications
    317      */
    318     protected Uri getContentNotificationUri() {
    319         throw new UnsupportedOperationException(
    320                 "Subclasses must override this method for content observation to work");
    321     }
    322 
    323     /**
    324      * This method is called when the underlying data has changed, and notifies registered observers
    325      * @param selfChange true if this is a self-change notification
    326      */
    327     @SuppressWarnings("deprecation")
    328     public synchronized void onChange(final boolean selfChange) {
    329         if (mObservable != null) {
    330             mObservable.dispatchChange(selfChange);
    331         }
    332     }
    333 
    334     /**
    335      * A content observer that calls {@link #onChange(boolean)} when triggered
    336      */
    337     private static class SelfContentObserver extends ContentObserver {
    338         WeakReference<EmailContent> mContent;
    339 
    340         public SelfContentObserver(final EmailContent content) {
    341             super(null);
    342             mContent = new WeakReference<EmailContent>(content);
    343         }
    344 
    345         @Override
    346         public boolean deliverSelfNotifications() {
    347             return false;
    348         }
    349 
    350         @Override
    351         public void onChange(final boolean selfChange) {
    352             EmailContent content = mContent.get();
    353             if (content != null) {
    354                 content.onChange(false);
    355             }
    356         }
    357     }
    358 
    359 
    360     // The Content sub class must have a no-arg constructor
    361     static public <T extends EmailContent> T getContent(final Context context, final Cursor cursor,
    362             final Class<T> klass) {
    363         try {
    364             T content = klass.newInstance();
    365             content.mId = cursor.getLong(0);
    366             content.restore(context, cursor);
    367             return content;
    368         } catch (IllegalAccessException e) {
    369             e.printStackTrace();
    370         } catch (InstantiationException e) {
    371             e.printStackTrace();
    372         }
    373         return null;
    374     }
    375 
    376     public Uri save(Context context) {
    377         if (isSaved()) {
    378             throw new UnsupportedOperationException();
    379         }
    380         Uri res = context.getContentResolver().insert(mBaseUri, toContentValues());
    381         mId = Long.parseLong(res.getPathSegments().get(1));
    382         return res;
    383     }
    384 
    385     public int update(Context context, ContentValues contentValues) {
    386         if (!isSaved()) {
    387             throw new UnsupportedOperationException();
    388         }
    389         return context.getContentResolver().update(getUri(), contentValues, null, null);
    390     }
    391 
    392     static public int update(Context context, Uri baseUri, long id, ContentValues contentValues) {
    393         return context.getContentResolver()
    394             .update(ContentUris.withAppendedId(baseUri, id), contentValues, null, null);
    395     }
    396 
    397     static public int delete(Context context, Uri baseUri, long id) {
    398         return context.getContentResolver()
    399             .delete(ContentUris.withAppendedId(baseUri, id), null, null);
    400     }
    401 
    402     /**
    403      * Generic count method that can be used for any ContentProvider
    404      *
    405      * @param context the calling Context
    406      * @param uri the Uri for the provider query
    407      * @param selection as with a query call
    408      * @param selectionArgs as with a query call
    409      * @return the number of items matching the query (or zero)
    410      */
    411     static public int count(Context context, Uri uri, String selection, String[] selectionArgs) {
    412         return Utility.getFirstRowLong(context,
    413                 uri, COUNT_COLUMNS, selection, selectionArgs, null, 0, 0L).intValue();
    414     }
    415 
    416     /**
    417      * Same as {@link #count(Context, Uri, String, String[])} without selection.
    418      */
    419     static public int count(Context context, Uri uri) {
    420         return count(context, uri, null, null);
    421     }
    422 
    423     static public Uri uriWithLimit(Uri uri, int limit) {
    424         return uri.buildUpon().appendQueryParameter(EmailContent.PARAMETER_LIMIT,
    425                 Integer.toString(limit)).build();
    426     }
    427 
    428     /**
    429      * no public constructor since this is a utility class
    430      */
    431     protected EmailContent() {
    432     }
    433 
    434     public interface SyncColumns {
    435         // source id (string) : the source's name of this item
    436         public static final String SERVER_ID = "syncServerId";
    437         // source's timestamp (long) for this item
    438         public static final String SERVER_TIMESTAMP = "syncServerTimeStamp";
    439     }
    440 
    441     public interface BodyColumns extends BaseColumns {
    442         // Foreign key to the message corresponding to this body
    443         public static final String MESSAGE_KEY = "messageKey";
    444         // The html content itself, not returned on query
    445         public static final String HTML_CONTENT = "htmlContent";
    446         // The html content URI, for ContentResolver#openFileDescriptor()
    447         public static final String HTML_CONTENT_URI = "htmlContentUri";
    448         // The plain text content itself, not returned on query
    449         public static final String TEXT_CONTENT = "textContent";
    450         // The text content URI, for ContentResolver#openFileDescriptor()
    451         public static final String TEXT_CONTENT_URI = "textContentUri";
    452         // Replied-to or forwarded body (in html form)
    453         @Deprecated
    454         public static final String HTML_REPLY = "htmlReply";
    455         // Replied-to or forwarded body (in text form)
    456         @Deprecated
    457         public static final String TEXT_REPLY = "textReply";
    458         // A reference to a message's unique id used in reply/forward.
    459         // Protocol code can be expected to use this column in determining whether a message can be
    460         // deleted safely (i.e. isn't referenced by other messages)
    461         public static final String SOURCE_MESSAGE_KEY = "sourceMessageKey";
    462         // The text to be placed between a reply/forward response and the original message
    463         @Deprecated
    464         public static final String INTRO_TEXT = "introText";
    465         // The start of quoted text within our text content
    466         public static final String QUOTED_TEXT_START_POS = "quotedTextStartPos";
    467     }
    468 
    469     public static final class Body extends EmailContent {
    470         public static final String TABLE_NAME = "Body";
    471 
    472         public static final String SELECTION_BY_MESSAGE_KEY = BodyColumns.MESSAGE_KEY + "=?";
    473 
    474         public static Uri CONTENT_URI;
    475 
    476         public static void initBody() {
    477             CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/body");
    478         }
    479 
    480         public static final String[] CONTENT_PROJECTION = new String[] {
    481                 BaseColumns._ID,
    482                 BodyColumns.MESSAGE_KEY,
    483                 BodyColumns.HTML_CONTENT_URI,
    484                 BodyColumns.TEXT_CONTENT_URI,
    485                 BodyColumns.SOURCE_MESSAGE_KEY,
    486                 BodyColumns.QUOTED_TEXT_START_POS
    487         };
    488 
    489         public static final int CONTENT_ID_COLUMN = 0;
    490         public static final int CONTENT_MESSAGE_KEY_COLUMN = 1;
    491         public static final int CONTENT_HTML_URI_COLUMN = 2;
    492         public static final int CONTENT_TEXT_URI_COLUMN = 3;
    493         public static final int CONTENT_SOURCE_KEY_COLUMN = 4;
    494         public static final int CONTENT_QUOTED_TEXT_START_POS_COLUMN = 5;
    495 
    496         public long mMessageKey;
    497         public String mHtmlContent;
    498         public String mTextContent;
    499         public int mQuotedTextStartPos;
    500 
    501         /**
    502          * Points to the ID of the message being replied to or forwarded. Will always be set.
    503          */
    504         public long mSourceKey;
    505 
    506         public Body() {
    507             mBaseUri = CONTENT_URI;
    508         }
    509 
    510         @Override
    511         public ContentValues toContentValues() {
    512             ContentValues values = new ContentValues();
    513 
    514             // Assign values for each row.
    515             values.put(BodyColumns.MESSAGE_KEY, mMessageKey);
    516             values.put(BodyColumns.HTML_CONTENT, mHtmlContent);
    517             values.put(BodyColumns.TEXT_CONTENT, mTextContent);
    518             values.put(BodyColumns.SOURCE_MESSAGE_KEY, mSourceKey);
    519             return values;
    520         }
    521 
    522         /**
    523          * Given a cursor, restore a Body from it
    524          * @param cursor a cursor which must NOT be null
    525          * @return the Body as restored from the cursor
    526          */
    527         private static Body restoreBodyWithCursor(final Context context, final Cursor cursor) {
    528             try {
    529                 if (cursor.moveToFirst()) {
    530                     return getContent(context, cursor, Body.class);
    531                 } else {
    532                     return null;
    533                 }
    534             } finally {
    535                 cursor.close();
    536             }
    537         }
    538 
    539         public static Body restoreBodyWithMessageId(Context context, long messageId) {
    540             Cursor c = context.getContentResolver().query(Body.CONTENT_URI,
    541                     Body.CONTENT_PROJECTION, BodyColumns.MESSAGE_KEY + "=?",
    542                     new String[] {Long.toString(messageId)}, null);
    543             if (c == null) throw new ProviderUnavailableException();
    544             return restoreBodyWithCursor(context, c);
    545         }
    546 
    547         /**
    548          * Returns the bodyId for the given messageId, or -1 if no body is found.
    549          */
    550         public static long lookupBodyIdWithMessageId(Context context, long messageId) {
    551             return Utility.getFirstRowLong(context, Body.CONTENT_URI,
    552                     ID_PROJECTION, BodyColumns.MESSAGE_KEY + "=?",
    553                     new String[] {Long.toString(messageId)}, null, ID_PROJECTION_COLUMN, -1L);
    554         }
    555 
    556         /**
    557          * Updates the Body for a messageId with the given ContentValues.
    558          * If the message has no body, a new body is inserted for the message.
    559          * Warning: the argument "values" is modified by this method, setting MESSAGE_KEY.
    560          */
    561         public static void updateBodyWithMessageId(Context context, long messageId,
    562                 ContentValues values) {
    563             ContentResolver resolver = context.getContentResolver();
    564             long bodyId = lookupBodyIdWithMessageId(context, messageId);
    565             values.put(BodyColumns.MESSAGE_KEY, messageId);
    566             if (bodyId == -1) {
    567                 resolver.insert(CONTENT_URI, values);
    568             } else {
    569                 final Uri uri = ContentUris.withAppendedId(CONTENT_URI, bodyId);
    570                 resolver.update(uri, values, null, null);
    571             }
    572         }
    573 
    574         @VisibleForTesting
    575         public static long restoreBodySourceKey(Context context, long messageId) {
    576             return Utility.getFirstRowLong(context, Body.CONTENT_URI,
    577                     new String[] {BodyColumns.SOURCE_MESSAGE_KEY},
    578                     BodyColumns.MESSAGE_KEY + "=?", new String[] {Long.toString(messageId)}, null,
    579                     0, 0L);
    580         }
    581 
    582         public static Uri getBodyTextUriForMessageWithId(long messageId) {
    583             return EmailContent.CONTENT_URI.buildUpon()
    584                     .appendPath("bodyText").appendPath(Long.toString(messageId)).build();
    585         }
    586 
    587         public static Uri getBodyHtmlUriForMessageWithId(long messageId) {
    588             return EmailContent.CONTENT_URI.buildUpon()
    589                     .appendPath("bodyHtml").appendPath(Long.toString(messageId)).build();
    590         }
    591 
    592         public static String restoreBodyTextWithMessageId(Context context, long messageId) {
    593             return readBodyFromProvider(context,
    594                     getBodyTextUriForMessageWithId(messageId).toString());
    595         }
    596 
    597         public static String restoreBodyHtmlWithMessageId(Context context, long messageId) {
    598             return readBodyFromProvider(context,
    599                     getBodyHtmlUriForMessageWithId(messageId).toString());
    600         }
    601 
    602         private static String readBodyFromProvider(final Context context, final String uri) {
    603             String content = null;
    604             try {
    605                 final InputStream bodyInput =
    606                         context.getContentResolver().openInputStream(Uri.parse(uri));
    607                 try {
    608                     content = IOUtils.toString(bodyInput);
    609                 } finally {
    610                     bodyInput.close();
    611                 }
    612             } catch (final IOException e) {
    613                 LogUtils.v(LogUtils.TAG, e, "Exception while reading body content");
    614             }
    615             return content;
    616         }
    617 
    618         @Override
    619         public void restore(final Cursor cursor) {
    620             throw new UnsupportedOperationException("Must have context to restore Body object");
    621         }
    622 
    623         @Override
    624         public void restore(final Context context, final Cursor cursor) {
    625             warnIfUiThread();
    626             mBaseUri = EmailContent.Body.CONTENT_URI;
    627             mMessageKey = cursor.getLong(CONTENT_MESSAGE_KEY_COLUMN);
    628             // These get overwritten below if we find a file descriptor in the respond() call,
    629             // but we'll keep this here in case we want to construct a matrix cursor or something
    630             // to build a Body object from.
    631             mHtmlContent = readBodyFromProvider(context, cursor.getString(CONTENT_HTML_URI_COLUMN));
    632             mTextContent = readBodyFromProvider(context, cursor.getString(CONTENT_TEXT_URI_COLUMN));
    633             mSourceKey = cursor.getLong(CONTENT_SOURCE_KEY_COLUMN);
    634             mQuotedTextStartPos = cursor.getInt(CONTENT_QUOTED_TEXT_START_POS_COLUMN);
    635         }
    636     }
    637 
    638     public interface MessageColumns extends BaseColumns, SyncColumns {
    639         // Basic columns used in message list presentation
    640         // The name as shown to the user in a message list
    641         public static final String DISPLAY_NAME = "displayName";
    642         // The time (millis) as shown to the user in a message list [INDEX]
    643         public static final String TIMESTAMP = "timeStamp";
    644         // Message subject
    645         public static final String SUBJECT = "subject";
    646         // Boolean, unread = 0, read = 1 [INDEX]
    647         public static final String FLAG_READ = "flagRead";
    648         // Load state, see constants below (unloaded, partial, complete, deleted)
    649         public static final String FLAG_LOADED = "flagLoaded";
    650         // Boolean, unflagged = 0, flagged (favorite) = 1
    651         public static final String FLAG_FAVORITE = "flagFavorite";
    652         // Boolean, no attachment = 0, attachment = 1
    653         public static final String FLAG_ATTACHMENT = "flagAttachment";
    654         // Bit field for flags which we'll not be selecting on
    655         public static final String FLAGS = "flags";
    656 
    657         // Sync related identifiers
    658         // Saved draft info (reusing the never-used "clientId" column)
    659         public static final String DRAFT_INFO = "clientId";
    660         // The message-id in the message's header
    661         public static final String MESSAGE_ID = "messageId";
    662 
    663         // References to other Email objects in the database
    664         // Foreign key to the Mailbox holding this message [INDEX]
    665         // TODO: This column is used in a complicated way: Usually, this refers to the mailbox
    666         // the server considers this message to be in. In the case of search results, this key
    667         // will refer to a special "search" mailbox, which does not exist on the server.
    668         // This is confusing and causes problems, see b/11294681.
    669         public static final String MAILBOX_KEY = "mailboxKey";
    670         // Foreign key to the Account holding this message
    671         public static final String ACCOUNT_KEY = "accountKey";
    672 
    673         // Address lists, packed with Address.pack()
    674         public static final String FROM_LIST = "fromList";
    675         public static final String TO_LIST = "toList";
    676         public static final String CC_LIST = "ccList";
    677         public static final String BCC_LIST = "bccList";
    678         public static final String REPLY_TO_LIST = "replyToList";
    679         // Meeting invitation related information (for now, start time in ms)
    680         public static final String MEETING_INFO = "meetingInfo";
    681         // A text "snippet" derived from the body of the message
    682         public static final String SNIPPET = "snippet";
    683         // A column that can be used by sync adapters to store search-related information about
    684         // a retrieved message (the messageKey for search results will be a TYPE_SEARCH mailbox
    685         // and the sync adapter might, for example, need more information about the original source
    686         // of the message)
    687         public static final String PROTOCOL_SEARCH_INFO = "protocolSearchInfo";
    688         // Simple thread topic
    689         public static final String THREAD_TOPIC = "threadTopic";
    690         // For sync adapter use
    691         public static final String SYNC_DATA = "syncData";
    692 
    693         /** Boolean, unseen = 0, seen = 1 [INDEX] */
    694         public static final String FLAG_SEEN = "flagSeen";
    695 
    696         // References to other Email objects in the database
    697         // Foreign key to the Mailbox holding this message [INDEX]
    698         // In cases where mailboxKey is NOT the real mailbox the server considers this message in,
    699         // this will be set. See b/11294681
    700         // We'd like to get rid of this column when the other changes mentioned in that bug
    701         // can be addressed.
    702         public static final String MAIN_MAILBOX_KEY = "mainMailboxKey";
    703 
    704     }
    705 
    706     public static final class Message extends EmailContent {
    707         private static final String LOG_TAG = "Email";
    708 
    709         public static final String TABLE_NAME = "Message";
    710         public static final String UPDATED_TABLE_NAME = "Message_Updates";
    711         public static final String DELETED_TABLE_NAME = "Message_Deletes";
    712 
    713         // To refer to a specific message, use ContentUris.withAppendedId(CONTENT_URI, id)
    714         public static Uri CONTENT_URI;
    715         public static Uri CONTENT_URI_LIMIT_1;
    716         public static Uri SYNCED_CONTENT_URI;
    717         public static Uri SELECTED_MESSAGE_CONTENT_URI ;
    718         public static Uri DELETED_CONTENT_URI;
    719         public static Uri UPDATED_CONTENT_URI;
    720         public static Uri NOTIFIER_URI;
    721 
    722         public static void initMessage() {
    723             CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/message");
    724             CONTENT_URI_LIMIT_1 = uriWithLimit(CONTENT_URI, 1);
    725             SYNCED_CONTENT_URI =
    726                     Uri.parse(EmailContent.CONTENT_URI + "/syncedMessage");
    727             SELECTED_MESSAGE_CONTENT_URI =
    728                     Uri.parse(EmailContent.CONTENT_URI + "/messageBySelection");
    729             DELETED_CONTENT_URI =
    730                     Uri.parse(EmailContent.CONTENT_URI + "/deletedMessage");
    731             UPDATED_CONTENT_URI =
    732                     Uri.parse(EmailContent.CONTENT_URI + "/updatedMessage");
    733             NOTIFIER_URI =
    734                     Uri.parse(EmailContent.CONTENT_NOTIFIER_URI + "/message");
    735         }
    736 
    737         public static final int CONTENT_ID_COLUMN = 0;
    738         public static final int CONTENT_DISPLAY_NAME_COLUMN = 1;
    739         public static final int CONTENT_TIMESTAMP_COLUMN = 2;
    740         public static final int CONTENT_SUBJECT_COLUMN = 3;
    741         public static final int CONTENT_FLAG_READ_COLUMN = 4;
    742         public static final int CONTENT_FLAG_LOADED_COLUMN = 5;
    743         public static final int CONTENT_FLAG_FAVORITE_COLUMN = 6;
    744         public static final int CONTENT_FLAG_ATTACHMENT_COLUMN = 7;
    745         public static final int CONTENT_FLAGS_COLUMN = 8;
    746         public static final int CONTENT_SERVER_ID_COLUMN = 9;
    747         public static final int CONTENT_DRAFT_INFO_COLUMN = 10;
    748         public static final int CONTENT_MESSAGE_ID_COLUMN = 11;
    749         public static final int CONTENT_MAILBOX_KEY_COLUMN = 12;
    750         public static final int CONTENT_ACCOUNT_KEY_COLUMN = 13;
    751         public static final int CONTENT_FROM_LIST_COLUMN = 14;
    752         public static final int CONTENT_TO_LIST_COLUMN = 15;
    753         public static final int CONTENT_CC_LIST_COLUMN = 16;
    754         public static final int CONTENT_BCC_LIST_COLUMN = 17;
    755         public static final int CONTENT_REPLY_TO_COLUMN = 18;
    756         public static final int CONTENT_SERVER_TIMESTAMP_COLUMN = 19;
    757         public static final int CONTENT_MEETING_INFO_COLUMN = 20;
    758         public static final int CONTENT_SNIPPET_COLUMN = 21;
    759         public static final int CONTENT_PROTOCOL_SEARCH_INFO_COLUMN = 22;
    760         public static final int CONTENT_THREAD_TOPIC_COLUMN = 23;
    761         public static final int CONTENT_SYNC_DATA_COLUMN = 24;
    762         public static final int CONTENT_FLAG_SEEN_COLUMN = 25;
    763         public static final int CONTENT_MAIN_MAILBOX_KEY_COLUMN = 26;
    764 
    765         public static final String[] CONTENT_PROJECTION = {
    766             MessageColumns._ID,
    767             MessageColumns.DISPLAY_NAME, MessageColumns.TIMESTAMP,
    768             MessageColumns.SUBJECT, MessageColumns.FLAG_READ,
    769             MessageColumns.FLAG_LOADED, MessageColumns.FLAG_FAVORITE,
    770             MessageColumns.FLAG_ATTACHMENT, MessageColumns.FLAGS,
    771             SyncColumns.SERVER_ID, MessageColumns.DRAFT_INFO,
    772             MessageColumns.MESSAGE_ID, MessageColumns.MAILBOX_KEY,
    773             MessageColumns.ACCOUNT_KEY, MessageColumns.FROM_LIST,
    774             MessageColumns.TO_LIST, MessageColumns.CC_LIST,
    775             MessageColumns.BCC_LIST, MessageColumns.REPLY_TO_LIST,
    776             SyncColumns.SERVER_TIMESTAMP, MessageColumns.MEETING_INFO,
    777             MessageColumns.SNIPPET, MessageColumns.PROTOCOL_SEARCH_INFO,
    778             MessageColumns.THREAD_TOPIC, MessageColumns.SYNC_DATA,
    779             MessageColumns.FLAG_SEEN, MessageColumns.MAIN_MAILBOX_KEY
    780         };
    781 
    782         public static final int LIST_ID_COLUMN = 0;
    783         public static final int LIST_DISPLAY_NAME_COLUMN = 1;
    784         public static final int LIST_TIMESTAMP_COLUMN = 2;
    785         public static final int LIST_SUBJECT_COLUMN = 3;
    786         public static final int LIST_READ_COLUMN = 4;
    787         public static final int LIST_LOADED_COLUMN = 5;
    788         public static final int LIST_FAVORITE_COLUMN = 6;
    789         public static final int LIST_ATTACHMENT_COLUMN = 7;
    790         public static final int LIST_FLAGS_COLUMN = 8;
    791         public static final int LIST_MAILBOX_KEY_COLUMN = 9;
    792         public static final int LIST_ACCOUNT_KEY_COLUMN = 10;
    793         public static final int LIST_SERVER_ID_COLUMN = 11;
    794         public static final int LIST_SNIPPET_COLUMN = 12;
    795 
    796         // Public projection for common list columns
    797         public static final String[] LIST_PROJECTION = {
    798             MessageColumns._ID,
    799             MessageColumns.DISPLAY_NAME, MessageColumns.TIMESTAMP,
    800             MessageColumns.SUBJECT, MessageColumns.FLAG_READ,
    801             MessageColumns.FLAG_LOADED, MessageColumns.FLAG_FAVORITE,
    802             MessageColumns.FLAG_ATTACHMENT, MessageColumns.FLAGS,
    803             MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
    804             SyncColumns.SERVER_ID, MessageColumns.SNIPPET
    805         };
    806 
    807         public static final int ID_COLUMNS_ID_COLUMN = 0;
    808         public static final int ID_COLUMNS_SYNC_SERVER_ID = 1;
    809         public static final String[] ID_COLUMNS_PROJECTION = {
    810                 MessageColumns._ID, SyncColumns.SERVER_ID
    811         };
    812 
    813         public static final String[] ID_COLUMN_PROJECTION = { MessageColumns._ID };
    814 
    815         public static final String ACCOUNT_KEY_SELECTION =
    816             MessageColumns.ACCOUNT_KEY + "=?";
    817 
    818         public static final String[] MAILBOX_KEY_PROJECTION = { MessageColumns.MAILBOX_KEY };
    819 
    820         /**
    821          * Selection for messages that are loaded
    822          *
    823          * POP messages at the initial stage have very little information. (Server UID only)
    824          * Use this to make sure they're not visible on any UI.
    825          * This means unread counts on the mailbox list can be different from the
    826          * number of messages in the message list, but it should be transient...
    827          */
    828         public static final String FLAG_LOADED_SELECTION =
    829             MessageColumns.FLAG_LOADED + " IN ("
    830             +     Message.FLAG_LOADED_PARTIAL + "," + Message.FLAG_LOADED_COMPLETE
    831             +     ")";
    832 
    833         public static final String ALL_FAVORITE_SELECTION =
    834             MessageColumns.FLAG_FAVORITE + "=1 AND "
    835             + MessageColumns.MAILBOX_KEY + " NOT IN ("
    836             +     "SELECT " + MailboxColumns._ID + " FROM " + Mailbox.TABLE_NAME + ""
    837             +     " WHERE " + MailboxColumns.TYPE + " = " + Mailbox.TYPE_TRASH
    838             +     ")"
    839             + " AND " + FLAG_LOADED_SELECTION;
    840 
    841         /** Selection to retrieve all messages in "inbox" for any account */
    842         public static final String ALL_INBOX_SELECTION =
    843             MessageColumns.MAILBOX_KEY + " IN ("
    844             +     "SELECT " + MailboxColumns._ID + " FROM " + Mailbox.TABLE_NAME
    845             +     " WHERE " + MailboxColumns.TYPE + " = " + Mailbox.TYPE_INBOX
    846             +     ")"
    847             + " AND " + FLAG_LOADED_SELECTION;
    848 
    849         /** Selection to retrieve all messages in "drafts" for any account */
    850         public static final String ALL_DRAFT_SELECTION =
    851             MessageColumns.MAILBOX_KEY + " IN ("
    852             +     "SELECT " + MailboxColumns._ID + " FROM " + Mailbox.TABLE_NAME
    853             +     " WHERE " + MailboxColumns.TYPE + " = " + Mailbox.TYPE_DRAFTS
    854             +     ")"
    855             + " AND " + FLAG_LOADED_SELECTION;
    856 
    857         /** Selection to retrieve all messages in "outbox" for any account */
    858         public static final String ALL_OUTBOX_SELECTION =
    859             MessageColumns.MAILBOX_KEY + " IN ("
    860             +     "SELECT " + MailboxColumns._ID + " FROM " + Mailbox.TABLE_NAME
    861             +     " WHERE " + MailboxColumns.TYPE + " = " + Mailbox.TYPE_OUTBOX
    862             +     ")"; // NOTE No flag_loaded test for outboxes.
    863 
    864         /** Selection to retrieve unread messages in "inbox" for any account */
    865         public static final String ALL_UNREAD_SELECTION =
    866             MessageColumns.FLAG_READ + "=0 AND " + ALL_INBOX_SELECTION;
    867 
    868         /** Selection to retrieve unread messages in "inbox" for one account */
    869         public static final String PER_ACCOUNT_UNREAD_SELECTION =
    870             ACCOUNT_KEY_SELECTION + " AND " + ALL_UNREAD_SELECTION;
    871 
    872         /** Selection to retrieve all messages in "inbox" for one account */
    873         public static final String PER_ACCOUNT_INBOX_SELECTION =
    874             ACCOUNT_KEY_SELECTION + " AND " + ALL_INBOX_SELECTION;
    875 
    876         public static final String PER_ACCOUNT_FAVORITE_SELECTION =
    877             ACCOUNT_KEY_SELECTION + " AND " + ALL_FAVORITE_SELECTION;
    878 
    879         public static final String MAILBOX_SELECTION = MessageColumns.MAILBOX_KEY + "=?";
    880 
    881         // _id field is in AbstractContent
    882         public String mDisplayName;
    883         public long mTimeStamp;
    884         public String mSubject;
    885         public boolean mFlagRead = false;
    886         public boolean mFlagSeen = false;
    887         public int mFlagLoaded = FLAG_LOADED_UNLOADED;
    888         public boolean mFlagFavorite = false;
    889         public boolean mFlagAttachment = false;
    890         public int mFlags = 0;
    891 
    892         public String mServerId;
    893         public long mServerTimeStamp;
    894         public int mDraftInfo;
    895         public String mMessageId;
    896 
    897         public long mMailboxKey;
    898         public long mAccountKey;
    899         public long mMainMailboxKey;
    900 
    901         public String mFrom;
    902         public String mTo;
    903         public String mCc;
    904         public String mBcc;
    905         public String mReplyTo;
    906 
    907         // For now, just the start time of a meeting invite, in ms
    908         public String mMeetingInfo;
    909 
    910         public String mSnippet;
    911 
    912         public String mProtocolSearchInfo;
    913 
    914         public String mThreadTopic;
    915 
    916         public String mSyncData;
    917 
    918         /**
    919          * Base64-encoded representation of the byte array provided by servers for identifying
    920          * messages belonging to the same conversation thread. Currently unsupported and not
    921          * persisted in the database.
    922          */
    923         public String mServerConversationId;
    924 
    925         // The following transient members may be used while building and manipulating messages,
    926         // but they are NOT persisted directly by EmailProvider. See Body for related fields.
    927         transient public String mText;
    928         transient public String mHtml;
    929         transient public long mSourceKey;
    930         transient public ArrayList<Attachment> mAttachments = null;
    931         transient public int mQuotedTextStartPos;
    932 
    933 
    934         // Values used in mFlagRead
    935         public static final int UNREAD = 0;
    936         public static final int READ = 1;
    937 
    938         // Values used in mFlagLoaded
    939         public static final int FLAG_LOADED_UNLOADED = 0;
    940         public static final int FLAG_LOADED_COMPLETE = 1;
    941         public static final int FLAG_LOADED_PARTIAL = 2;
    942         public static final int FLAG_LOADED_DELETED = 3;
    943         public static final int FLAG_LOADED_UNKNOWN = 4;
    944 
    945         // Bits used in mFlags
    946         // The following three states are mutually exclusive, and indicate whether the message is an
    947         // original, a reply, or a forward
    948         public static final int FLAG_TYPE_REPLY = 1<<0;
    949         public static final int FLAG_TYPE_FORWARD = 1<<1;
    950         public static final int FLAG_TYPE_MASK = FLAG_TYPE_REPLY | FLAG_TYPE_FORWARD;
    951         // The following flags indicate messages that are determined to be incoming meeting related
    952         // (e.g. invites from others)
    953         public static final int FLAG_INCOMING_MEETING_INVITE = 1<<2;
    954         public static final int FLAG_INCOMING_MEETING_CANCEL = 1<<3;
    955         public static final int FLAG_INCOMING_MEETING_MASK =
    956             FLAG_INCOMING_MEETING_INVITE | FLAG_INCOMING_MEETING_CANCEL;
    957         // The following flags indicate messages that are outgoing and meeting related
    958         // (e.g. invites TO others)
    959         public static final int FLAG_OUTGOING_MEETING_INVITE = 1<<4;
    960         public static final int FLAG_OUTGOING_MEETING_CANCEL = 1<<5;
    961         public static final int FLAG_OUTGOING_MEETING_ACCEPT = 1<<6;
    962         public static final int FLAG_OUTGOING_MEETING_DECLINE = 1<<7;
    963         public static final int FLAG_OUTGOING_MEETING_TENTATIVE = 1<<8;
    964         public static final int FLAG_OUTGOING_MEETING_MASK =
    965             FLAG_OUTGOING_MEETING_INVITE | FLAG_OUTGOING_MEETING_CANCEL |
    966             FLAG_OUTGOING_MEETING_ACCEPT | FLAG_OUTGOING_MEETING_DECLINE |
    967             FLAG_OUTGOING_MEETING_TENTATIVE;
    968         public static final int FLAG_OUTGOING_MEETING_REQUEST_MASK =
    969             FLAG_OUTGOING_MEETING_INVITE | FLAG_OUTGOING_MEETING_CANCEL;
    970         // 8 general purpose flags (bits) that may be used at the discretion of the sync adapter
    971         public static final int FLAG_SYNC_ADAPTER_SHIFT = 9;
    972         public static final int FLAG_SYNC_ADAPTER_MASK = 255 << FLAG_SYNC_ADAPTER_SHIFT;
    973         /** If set, the outgoing message should *not* include the quoted original message. */
    974         public static final int FLAG_NOT_INCLUDE_QUOTED_TEXT = 1 << 17;
    975         public static final int FLAG_REPLIED_TO = 1 << 18;
    976         public static final int FLAG_FORWARDED = 1 << 19;
    977 
    978         // Outgoing, original message
    979         public static final int FLAG_TYPE_ORIGINAL = 1 << 20;
    980         // Outgoing, reply all message; note, FLAG_TYPE_REPLY should also be set for backward
    981         // compatibility
    982         public static final int FLAG_TYPE_REPLY_ALL = 1 << 21;
    983 
    984         // Flag used in draftInfo to indicate that the reference message should be appended
    985         public static final int DRAFT_INFO_APPEND_REF_MESSAGE = 1 << 24;
    986         public static final int DRAFT_INFO_QUOTE_POS_MASK = 0xFFFFFF;
    987 
    988         /** a pseudo ID for "no message". */
    989         public static final long NO_MESSAGE = -1L;
    990 
    991         private static final int ATTACHMENT_INDEX_OFFSET = 2;
    992 
    993         public Message() {
    994             mBaseUri = CONTENT_URI;
    995         }
    996 
    997         @Override
    998         public ContentValues toContentValues() {
    999             ContentValues values = new ContentValues();
   1000 
   1001             // Assign values for each row.
   1002             values.put(MessageColumns.DISPLAY_NAME, mDisplayName);
   1003             values.put(MessageColumns.TIMESTAMP, mTimeStamp);
   1004             values.put(MessageColumns.SUBJECT, mSubject);
   1005             values.put(MessageColumns.FLAG_READ, mFlagRead);
   1006             values.put(MessageColumns.FLAG_SEEN, mFlagSeen);
   1007             values.put(MessageColumns.FLAG_LOADED, mFlagLoaded);
   1008             values.put(MessageColumns.FLAG_FAVORITE, mFlagFavorite);
   1009             values.put(MessageColumns.FLAG_ATTACHMENT, mFlagAttachment);
   1010             values.put(MessageColumns.FLAGS, mFlags);
   1011             values.put(SyncColumns.SERVER_ID, mServerId);
   1012             values.put(SyncColumns.SERVER_TIMESTAMP, mServerTimeStamp);
   1013             values.put(MessageColumns.DRAFT_INFO, mDraftInfo);
   1014             values.put(MessageColumns.MESSAGE_ID, mMessageId);
   1015             values.put(MessageColumns.MAILBOX_KEY, mMailboxKey);
   1016             values.put(MessageColumns.ACCOUNT_KEY, mAccountKey);
   1017             values.put(MessageColumns.FROM_LIST, mFrom);
   1018             values.put(MessageColumns.TO_LIST, mTo);
   1019             values.put(MessageColumns.CC_LIST, mCc);
   1020             values.put(MessageColumns.BCC_LIST, mBcc);
   1021             values.put(MessageColumns.REPLY_TO_LIST, mReplyTo);
   1022             values.put(MessageColumns.MEETING_INFO, mMeetingInfo);
   1023             values.put(MessageColumns.SNIPPET, mSnippet);
   1024             values.put(MessageColumns.PROTOCOL_SEARCH_INFO, mProtocolSearchInfo);
   1025             values.put(MessageColumns.THREAD_TOPIC, mThreadTopic);
   1026             values.put(MessageColumns.SYNC_DATA, mSyncData);
   1027             values.put(MessageColumns.MAIN_MAILBOX_KEY, mMainMailboxKey);
   1028             return values;
   1029         }
   1030 
   1031         public static Message restoreMessageWithId(Context context, long id) {
   1032             return EmailContent.restoreContentWithId(context, Message.class,
   1033                     Message.CONTENT_URI, Message.CONTENT_PROJECTION, id);
   1034         }
   1035 
   1036         @Override
   1037         public void restore(Cursor cursor) {
   1038             mBaseUri = CONTENT_URI;
   1039             mId = cursor.getLong(CONTENT_ID_COLUMN);
   1040             mDisplayName = cursor.getString(CONTENT_DISPLAY_NAME_COLUMN);
   1041             mTimeStamp = cursor.getLong(CONTENT_TIMESTAMP_COLUMN);
   1042             mSubject = cursor.getString(CONTENT_SUBJECT_COLUMN);
   1043             mFlagRead = cursor.getInt(CONTENT_FLAG_READ_COLUMN) == 1;
   1044             mFlagSeen = cursor.getInt(CONTENT_FLAG_SEEN_COLUMN) == 1;
   1045             mFlagLoaded = cursor.getInt(CONTENT_FLAG_LOADED_COLUMN);
   1046             mFlagFavorite = cursor.getInt(CONTENT_FLAG_FAVORITE_COLUMN) == 1;
   1047             mFlagAttachment = cursor.getInt(CONTENT_FLAG_ATTACHMENT_COLUMN) == 1;
   1048             mFlags = cursor.getInt(CONTENT_FLAGS_COLUMN);
   1049             mServerId = cursor.getString(CONTENT_SERVER_ID_COLUMN);
   1050             mServerTimeStamp = cursor.getLong(CONTENT_SERVER_TIMESTAMP_COLUMN);
   1051             mDraftInfo = cursor.getInt(CONTENT_DRAFT_INFO_COLUMN);
   1052             mMessageId = cursor.getString(CONTENT_MESSAGE_ID_COLUMN);
   1053             mMailboxKey = cursor.getLong(CONTENT_MAILBOX_KEY_COLUMN);
   1054             mMainMailboxKey = cursor.getLong(CONTENT_MAIN_MAILBOX_KEY_COLUMN);
   1055             mAccountKey = cursor.getLong(CONTENT_ACCOUNT_KEY_COLUMN);
   1056             mFrom = cursor.getString(CONTENT_FROM_LIST_COLUMN);
   1057             mTo = cursor.getString(CONTENT_TO_LIST_COLUMN);
   1058             mCc = cursor.getString(CONTENT_CC_LIST_COLUMN);
   1059             mBcc = cursor.getString(CONTENT_BCC_LIST_COLUMN);
   1060             mReplyTo = cursor.getString(CONTENT_REPLY_TO_COLUMN);
   1061             mMeetingInfo = cursor.getString(CONTENT_MEETING_INFO_COLUMN);
   1062             mSnippet = cursor.getString(CONTENT_SNIPPET_COLUMN);
   1063             mProtocolSearchInfo = cursor.getString(CONTENT_PROTOCOL_SEARCH_INFO_COLUMN);
   1064             mThreadTopic = cursor.getString(CONTENT_THREAD_TOPIC_COLUMN);
   1065             mSyncData = cursor.getString(CONTENT_SYNC_DATA_COLUMN);
   1066         }
   1067 
   1068         /*
   1069          * Override this so that we can store the Body first and link it to the Message
   1070          * Also, attachments when we get there...
   1071          * (non-Javadoc)
   1072          * @see com.android.email.provider.EmailContent#save(android.content.Context)
   1073          */
   1074         @Override
   1075         public Uri save(Context context) {
   1076 
   1077             boolean doSave = !isSaved();
   1078 
   1079             // This logic is in place so I can (a) short circuit the expensive stuff when
   1080             // possible, and (b) override (and throw) if anyone tries to call save() or update()
   1081             // directly for Message, which are unsupported.
   1082             if (mText == null && mHtml == null &&
   1083                     (mAttachments == null || mAttachments.isEmpty())) {
   1084                 if (doSave) {
   1085                     return super.save(context);
   1086                 } else {
   1087                     // FLAG: Should we be doing this? In the base class, if someone calls "save" on
   1088                     // an EmailContent that is already saved, it throws an exception.
   1089                     // Call update, rather than super.update in case we ever override it
   1090                     if (update(context, toContentValues()) == 1) {
   1091                         return getUri();
   1092                     }
   1093                     return null;
   1094                 }
   1095             }
   1096 
   1097             final ArrayList<ContentProviderOperation> ops =
   1098                     new ArrayList<ContentProviderOperation>();
   1099             addSaveOps(ops);
   1100             try {
   1101                 final ContentProviderResult[] results =
   1102                     context.getContentResolver().applyBatch(AUTHORITY, ops);
   1103                 // If saving, set the mId's of the various saved objects
   1104                 if (doSave) {
   1105                     Uri u = results[0].uri;
   1106                     mId = Long.parseLong(u.getPathSegments().get(1));
   1107                     if (mAttachments != null) {
   1108                         // Skip over the first two items in the result array
   1109                         for (int i = 0; i < mAttachments.size(); i++) {
   1110                             final Attachment a = mAttachments.get(i);
   1111 
   1112                             final int resultIndex = i + ATTACHMENT_INDEX_OFFSET;
   1113                             // Save the id of the attachment record
   1114                             if (resultIndex < results.length) {
   1115                                 u = results[resultIndex].uri;
   1116                             } else {
   1117                                 // We didn't find the expected attachment, log this error
   1118                                 LogUtils.e(LOG_TAG, "Invalid index into ContentProviderResults: " +
   1119                                         resultIndex);
   1120                                 u = null;
   1121                             }
   1122                             if (u != null) {
   1123                                 a.mId = Long.parseLong(u.getPathSegments().get(1));
   1124                             }
   1125                             a.mMessageKey = mId;
   1126                         }
   1127                     }
   1128                     return u;
   1129                 } else {
   1130                     return null;
   1131                 }
   1132             } catch (RemoteException e) {
   1133                 // There is nothing to be done here; fail by returning null
   1134             } catch (OperationApplicationException e) {
   1135                 // There is nothing to be done here; fail by returning null
   1136             }
   1137             return null;
   1138         }
   1139 
   1140         /**
   1141          * Save or update a message
   1142          * @param ops an array of CPOs that we'll add to
   1143          */
   1144         public void addSaveOps(ArrayList<ContentProviderOperation> ops) {
   1145             boolean isNew = !isSaved();
   1146             ContentProviderOperation.Builder b;
   1147             // First, save/update the message
   1148             if (isNew) {
   1149                 b = ContentProviderOperation.newInsert(mBaseUri);
   1150             } else {
   1151                 b = ContentProviderOperation.newUpdate(mBaseUri)
   1152                         .withSelection(MessageColumns._ID + "=?",
   1153                                 new String[] {Long.toString(mId)});
   1154             }
   1155             // Generate the snippet here, before we create the CPO for Message
   1156             if (mText != null) {
   1157                 mSnippet = TextUtilities.makeSnippetFromPlainText(mText);
   1158             } else if (mHtml != null) {
   1159                 mSnippet = TextUtilities.makeSnippetFromHtmlText(mHtml);
   1160             }
   1161             ops.add(b.withValues(toContentValues()).build());
   1162 
   1163             // Create and save the body
   1164             ContentValues cv = new ContentValues();
   1165             if (mText != null) {
   1166                 cv.put(BodyColumns.TEXT_CONTENT, mText);
   1167             }
   1168             if (mHtml != null) {
   1169                 cv.put(BodyColumns.HTML_CONTENT, mHtml);
   1170             }
   1171             if (mSourceKey != 0) {
   1172                 cv.put(BodyColumns.SOURCE_MESSAGE_KEY, mSourceKey);
   1173             }
   1174             if (mQuotedTextStartPos != 0) {
   1175                 cv.put(BodyColumns.QUOTED_TEXT_START_POS, mQuotedTextStartPos);
   1176             }
   1177             // We'll need this if we're new
   1178             int messageBackValue = ops.size() - 1;
   1179             // Only create a body if we've got some data
   1180             if (!cv.keySet().isEmpty()) {
   1181                 b = ContentProviderOperation.newInsert(Body.CONTENT_URI);
   1182                 // Put our message id in the Body
   1183                 if (!isNew) {
   1184                     cv.put(BodyColumns.MESSAGE_KEY, mId);
   1185                 }
   1186                 b.withValues(cv);
   1187                 // If we're new, create a back value entry
   1188                 if (isNew) {
   1189                     ContentValues backValues = new ContentValues();
   1190                     backValues.put(BodyColumns.MESSAGE_KEY, messageBackValue);
   1191                     b.withValueBackReferences(backValues);
   1192                 }
   1193                 // And add the Body operation
   1194                 ops.add(b.build());
   1195             }
   1196 
   1197             // Create the attaachments, if any
   1198             if (mAttachments != null) {
   1199                 for (Attachment att: mAttachments) {
   1200                     if (!isNew) {
   1201                         att.mMessageKey = mId;
   1202                     }
   1203                     b = ContentProviderOperation.newInsert(Attachment.CONTENT_URI)
   1204                             .withValues(att.toContentValues());
   1205                     if (isNew) {
   1206                         b.withValueBackReference(AttachmentColumns.MESSAGE_KEY, messageBackValue);
   1207                     }
   1208                     ops.add(b.build());
   1209                 }
   1210             }
   1211         }
   1212 
   1213         /**
   1214          * @return number of favorite (starred) messages throughout all accounts.
   1215          */
   1216         public static int getFavoriteMessageCount(Context context) {
   1217             return count(context, Message.CONTENT_URI, ALL_FAVORITE_SELECTION, null);
   1218         }
   1219 
   1220         /**
   1221          * @return number of favorite (starred) messages for an account
   1222          */
   1223         public static int getFavoriteMessageCount(Context context, long accountId) {
   1224             return count(context, Message.CONTENT_URI, PER_ACCOUNT_FAVORITE_SELECTION,
   1225                     new String[]{Long.toString(accountId)});
   1226         }
   1227 
   1228         public static long getKeyColumnLong(Context context, long messageId, String column) {
   1229             String[] columns =
   1230                 Utility.getRowColumns(context, Message.CONTENT_URI, messageId, column);
   1231             if (columns != null && columns[0] != null) {
   1232                 return Long.parseLong(columns[0]);
   1233             }
   1234             return -1;
   1235         }
   1236 
   1237         /**
   1238          * Returns the where clause for a message list selection.
   1239          *
   1240          * Accesses the detabase to determine the mailbox type.  DO NOT CALL FROM UI THREAD.
   1241          */
   1242         public static String buildMessageListSelection(
   1243                 Context context, long accountId, long mailboxId) {
   1244 
   1245             if (mailboxId == Mailbox.QUERY_ALL_INBOXES) {
   1246                 return Message.ALL_INBOX_SELECTION;
   1247             }
   1248             if (mailboxId == Mailbox.QUERY_ALL_DRAFTS) {
   1249                 return Message.ALL_DRAFT_SELECTION;
   1250             }
   1251             if (mailboxId == Mailbox.QUERY_ALL_OUTBOX) {
   1252                 return Message.ALL_OUTBOX_SELECTION;
   1253             }
   1254             if (mailboxId == Mailbox.QUERY_ALL_UNREAD) {
   1255                 return Message.ALL_UNREAD_SELECTION;
   1256             }
   1257             // TODO: we only support per-account starred mailbox right now, but presumably, we
   1258             // can surface the same thing for unread.
   1259             if (mailboxId == Mailbox.QUERY_ALL_FAVORITES) {
   1260                 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
   1261                     return Message.ALL_FAVORITE_SELECTION;
   1262                 }
   1263 
   1264                 final StringBuilder selection = new StringBuilder();
   1265                 selection.append(MessageColumns.ACCOUNT_KEY).append('=').append(accountId)
   1266                         .append(" AND ")
   1267                         .append(Message.ALL_FAVORITE_SELECTION);
   1268                 return selection.toString();
   1269             }
   1270 
   1271             // Now it's a regular mailbox.
   1272             final StringBuilder selection = new StringBuilder();
   1273 
   1274             selection.append(MessageColumns.MAILBOX_KEY).append('=').append(mailboxId);
   1275 
   1276             if (Mailbox.getMailboxType(context, mailboxId) != Mailbox.TYPE_OUTBOX) {
   1277                 selection.append(" AND ").append(Message.FLAG_LOADED_SELECTION);
   1278             }
   1279             return selection.toString();
   1280         }
   1281 
   1282         public void setFlags(boolean quotedReply, boolean quotedForward) {
   1283             // Set message flags as well
   1284             if (quotedReply || quotedForward) {
   1285                 mFlags &= ~Message.FLAG_TYPE_MASK;
   1286                 mFlags |= quotedReply
   1287                         ? Message.FLAG_TYPE_REPLY
   1288                         : Message.FLAG_TYPE_FORWARD;
   1289             }
   1290         }
   1291     }
   1292 
   1293     public interface AttachmentColumns extends BaseColumns {
   1294         // The display name of the attachment
   1295         public static final String FILENAME = "fileName";
   1296         // The mime type of the attachment
   1297         public static final String MIME_TYPE = "mimeType";
   1298         // The size of the attachment in bytes
   1299         public static final String SIZE = "size";
   1300         // The (internal) contentId of the attachment (inline attachments will have these)
   1301         public static final String CONTENT_ID = "contentId";
   1302         // The location of the loaded attachment (probably a file)
   1303         @SuppressWarnings("hiding")
   1304         public static final String CONTENT_URI = "contentUri";
   1305         // The cached location of the attachment
   1306         public static final String CACHED_FILE = "cachedFile";
   1307         // A foreign key into the Message table (the message owning this attachment)
   1308         public static final String MESSAGE_KEY = "messageKey";
   1309         // The location of the attachment on the server side
   1310         // For IMAP, this is a part number (e.g. 2.1); for EAS, it's the internal file name
   1311         public static final String LOCATION = "location";
   1312         // The transfer encoding of the attachment
   1313         public static final String ENCODING = "encoding";
   1314         // Not currently used
   1315         public static final String CONTENT = "content";
   1316         // Flags
   1317         public static final String FLAGS = "flags";
   1318         // Content that is actually contained in the Attachment row
   1319         public static final String CONTENT_BYTES = "content_bytes";
   1320         // A foreign key into the Account table (for the message owning this attachment)
   1321         public static final String ACCOUNT_KEY = "accountKey";
   1322         // The UIProvider state of the attachment
   1323         public static final String UI_STATE = "uiState";
   1324         // The UIProvider destination of the attachment
   1325         public static final String UI_DESTINATION = "uiDestination";
   1326         // The UIProvider downloaded size of the attachment
   1327         public static final String UI_DOWNLOADED_SIZE = "uiDownloadedSize";
   1328     }
   1329 
   1330     public static final class Attachment extends EmailContent implements Parcelable {
   1331         public static final String TABLE_NAME = "Attachment";
   1332         public static final String ATTACHMENT_PROVIDER_LEGACY_URI_PREFIX =
   1333                 "content://com.android.email.attachmentprovider";
   1334 
   1335         public static final String CACHED_FILE_QUERY_PARAM = "filePath";
   1336 
   1337         public static Uri CONTENT_URI;
   1338         // This must be used with an appended id: ContentUris.withAppendedId(MESSAGE_ID_URI, id)
   1339         public static Uri MESSAGE_ID_URI;
   1340         public static String ATTACHMENT_PROVIDER_URI_PREFIX;
   1341         public static String ATTACHMENT_PROVIDER_AUTHORITY;
   1342         public static boolean sUsingLegacyPrefix;
   1343 
   1344         public static void initAttachment() {
   1345             CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/attachment");
   1346             MESSAGE_ID_URI = Uri.parse(
   1347                     EmailContent.CONTENT_URI + "/attachment/message");
   1348             ATTACHMENT_PROVIDER_AUTHORITY = EmailContent.EMAIL_PACKAGE_NAME +
   1349                     ".attachmentprovider";
   1350             ATTACHMENT_PROVIDER_URI_PREFIX = "content://" + ATTACHMENT_PROVIDER_AUTHORITY;
   1351             sUsingLegacyPrefix =
   1352                     ATTACHMENT_PROVIDER_URI_PREFIX.equals(ATTACHMENT_PROVIDER_LEGACY_URI_PREFIX);
   1353         }
   1354 
   1355         public String mFileName;
   1356         public String mMimeType;
   1357         public long mSize;
   1358         public String mContentId;
   1359         private String mContentUri;
   1360         private String mCachedFileUri;
   1361         public long mMessageKey;
   1362         public String mLocation;
   1363         public String mEncoding;
   1364         public String mContent; // Not currently used
   1365         public int mFlags;
   1366         public byte[] mContentBytes;
   1367         public long mAccountKey;
   1368         public int mUiState;
   1369         public int mUiDestination;
   1370         public int mUiDownloadedSize;
   1371 
   1372         public static final int CONTENT_ID_COLUMN = 0;
   1373         public static final int CONTENT_FILENAME_COLUMN = 1;
   1374         public static final int CONTENT_MIME_TYPE_COLUMN = 2;
   1375         public static final int CONTENT_SIZE_COLUMN = 3;
   1376         public static final int CONTENT_CONTENT_ID_COLUMN = 4;
   1377         public static final int CONTENT_CONTENT_URI_COLUMN = 5;
   1378         public static final int CONTENT_CACHED_FILE_COLUMN = 6;
   1379         public static final int CONTENT_MESSAGE_ID_COLUMN = 7;
   1380         public static final int CONTENT_LOCATION_COLUMN = 8;
   1381         public static final int CONTENT_ENCODING_COLUMN = 9;
   1382         public static final int CONTENT_CONTENT_COLUMN = 10; // Not currently used
   1383         public static final int CONTENT_FLAGS_COLUMN = 11;
   1384         public static final int CONTENT_CONTENT_BYTES_COLUMN = 12;
   1385         public static final int CONTENT_ACCOUNT_KEY_COLUMN = 13;
   1386         public static final int CONTENT_UI_STATE_COLUMN = 14;
   1387         public static final int CONTENT_UI_DESTINATION_COLUMN = 15;
   1388         public static final int CONTENT_UI_DOWNLOADED_SIZE_COLUMN = 16;
   1389         public static final String[] CONTENT_PROJECTION = {
   1390             AttachmentColumns._ID, AttachmentColumns.FILENAME, AttachmentColumns.MIME_TYPE,
   1391             AttachmentColumns.SIZE, AttachmentColumns.CONTENT_ID, AttachmentColumns.CONTENT_URI,
   1392             AttachmentColumns.CACHED_FILE, AttachmentColumns.MESSAGE_KEY,
   1393             AttachmentColumns.LOCATION, AttachmentColumns.ENCODING, AttachmentColumns.CONTENT,
   1394             AttachmentColumns.FLAGS, AttachmentColumns.CONTENT_BYTES, AttachmentColumns.ACCOUNT_KEY,
   1395             AttachmentColumns.UI_STATE, AttachmentColumns.UI_DESTINATION,
   1396             AttachmentColumns.UI_DOWNLOADED_SIZE
   1397         };
   1398 
   1399         // All attachments with an empty URI, regardless of mailbox
   1400         public static final String PRECACHE_SELECTION =
   1401             AttachmentColumns.CONTENT_URI + " isnull AND " + AttachmentColumns.FLAGS + "=0";
   1402         // Attachments with an empty URI that are in an inbox
   1403         public static final String PRECACHE_INBOX_SELECTION =
   1404             PRECACHE_SELECTION + " AND " + AttachmentColumns.MESSAGE_KEY + " IN ("
   1405             +     "SELECT " + MessageColumns._ID + " FROM " + Message.TABLE_NAME
   1406             +     " WHERE " + Message.ALL_INBOX_SELECTION
   1407             +     ")";
   1408 
   1409         // Bits used in mFlags
   1410         // WARNING: AttachmentService relies on the fact that ALL of the flags below
   1411         // disqualify attachments for precaching.  If you add a flag that does NOT disqualify an
   1412         // attachment for precaching, you MUST change the PRECACHE_SELECTION definition above
   1413 
   1414         // Instruct Rfc822Output to 1) not use Content-Disposition and 2) use multipart/alternative
   1415         // with this attachment.  This is only valid if there is one and only one attachment and
   1416         // that attachment has this flag set
   1417         public static final int FLAG_ICS_ALTERNATIVE_PART = 1<<0;
   1418         // Indicate that this attachment has been requested for downloading by the user; this is
   1419         // the highest priority for attachment downloading
   1420         public static final int FLAG_DOWNLOAD_USER_REQUEST = 1<<1;
   1421         // Indicate that this attachment needs to be downloaded as part of an outgoing forwarded
   1422         // message
   1423         public static final int FLAG_DOWNLOAD_FORWARD = 1<<2;
   1424         // Indicates that the attachment download failed in a non-recoverable manner
   1425         public static final int FLAG_DOWNLOAD_FAILED = 1<<3;
   1426         // Allow "room" for some additional download-related flags here
   1427         // Indicates that the attachment will be smart-forwarded
   1428         public static final int FLAG_SMART_FORWARD = 1<<8;
   1429         // Indicates that the attachment cannot be forwarded due to a policy restriction
   1430         public static final int FLAG_POLICY_DISALLOWS_DOWNLOAD = 1<<9;
   1431         // Indicates that this is a dummy placeholder attachment.
   1432         public static final int FLAG_DUMMY_ATTACHMENT = 1<<10;
   1433 
   1434         /**
   1435          * no public constructor since this is a utility class
   1436          */
   1437         public Attachment() {
   1438             mBaseUri = CONTENT_URI;
   1439         }
   1440 
   1441         public void setCachedFileUri(String cachedFile) {
   1442             mCachedFileUri = cachedFile;
   1443         }
   1444 
   1445         public String getCachedFileUri() {
   1446             return mCachedFileUri;
   1447         }
   1448 
   1449         public void setContentUri(String contentUri) {
   1450             mContentUri = contentUri;
   1451         }
   1452 
   1453         public String getContentUri() {
   1454             if (mContentUri == null) return null; //
   1455             // If we're not using the legacy prefix and the uri IS, we need to modify it
   1456             if (!Attachment.sUsingLegacyPrefix &&
   1457                     mContentUri.startsWith(Attachment.ATTACHMENT_PROVIDER_LEGACY_URI_PREFIX)) {
   1458                 // In an upgrade scenario, we may still have legacy attachment Uri's
   1459                 // Skip past content://
   1460                 int prefix = mContentUri.indexOf('/', 10);
   1461                 if (prefix > 0) {
   1462                     // Create a proper uri string using the actual provider
   1463                     return ATTACHMENT_PROVIDER_URI_PREFIX + "/" + mContentUri.substring(prefix);
   1464                 } else {
   1465                     LogUtils.e("Attachment", "Improper contentUri format: " + mContentUri);
   1466                     // Belt & suspenders; can't really happen
   1467                     return mContentUri;
   1468                 }
   1469             } else {
   1470                 return mContentUri;
   1471             }
   1472         }
   1473 
   1474          /**
   1475          * Restore an Attachment from the database, given its unique id
   1476          * @param context
   1477          * @param id
   1478          * @return the instantiated Attachment
   1479          */
   1480         public static Attachment restoreAttachmentWithId(Context context, long id) {
   1481             return EmailContent.restoreContentWithId(context, Attachment.class,
   1482                     Attachment.CONTENT_URI, Attachment.CONTENT_PROJECTION, id);
   1483         }
   1484 
   1485         /**
   1486          * Restore all the Attachments of a message given its messageId
   1487          */
   1488         public static Attachment[] restoreAttachmentsWithMessageId(Context context,
   1489                 long messageId) {
   1490             Uri uri = ContentUris.withAppendedId(MESSAGE_ID_URI, messageId);
   1491             Cursor c = context.getContentResolver().query(uri, CONTENT_PROJECTION,
   1492                     null, null, null);
   1493             try {
   1494                 int count = c.getCount();
   1495                 Attachment[] attachments = new Attachment[count];
   1496                 for (int i = 0; i < count; ++i) {
   1497                     c.moveToNext();
   1498                     Attachment attach = new Attachment();
   1499                     attach.restore(c);
   1500                     attachments[i] = attach;
   1501                 }
   1502                 return attachments;
   1503             } finally {
   1504                 c.close();
   1505             }
   1506         }
   1507 
   1508         /**
   1509          * Creates a unique file in the external store by appending a hyphen
   1510          * and a number to the given filename.
   1511          * @param filename
   1512          * @return a new File object, or null if one could not be created
   1513          */
   1514         public static File createUniqueFile(String filename) {
   1515             // TODO Handle internal storage, as required
   1516             if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
   1517                 File directory = Environment.getExternalStorageDirectory();
   1518                 File file = new File(directory, filename);
   1519                 if (!file.exists()) {
   1520                     return file;
   1521                 }
   1522                 // Get the extension of the file, if any.
   1523                 int index = filename.lastIndexOf('.');
   1524                 String name = filename;
   1525                 String extension = "";
   1526                 if (index != -1) {
   1527                     name = filename.substring(0, index);
   1528                     extension = filename.substring(index);
   1529                 }
   1530                 for (int i = 2; i < Integer.MAX_VALUE; i++) {
   1531                     file = new File(directory, name + '-' + i + extension);
   1532                     if (!file.exists()) {
   1533                         return file;
   1534                     }
   1535                 }
   1536                 return null;
   1537             }
   1538             return null;
   1539         }
   1540 
   1541         @Override
   1542         public void restore(Cursor cursor) {
   1543             mBaseUri = CONTENT_URI;
   1544             mId = cursor.getLong(CONTENT_ID_COLUMN);
   1545             mFileName= cursor.getString(CONTENT_FILENAME_COLUMN);
   1546             mMimeType = cursor.getString(CONTENT_MIME_TYPE_COLUMN);
   1547             mSize = cursor.getLong(CONTENT_SIZE_COLUMN);
   1548             mContentId = cursor.getString(CONTENT_CONTENT_ID_COLUMN);
   1549             mContentUri = cursor.getString(CONTENT_CONTENT_URI_COLUMN);
   1550             mCachedFileUri = cursor.getString(CONTENT_CACHED_FILE_COLUMN);
   1551             mMessageKey = cursor.getLong(CONTENT_MESSAGE_ID_COLUMN);
   1552             mLocation = cursor.getString(CONTENT_LOCATION_COLUMN);
   1553             mEncoding = cursor.getString(CONTENT_ENCODING_COLUMN);
   1554             mContent = cursor.getString(CONTENT_CONTENT_COLUMN);
   1555             mFlags = cursor.getInt(CONTENT_FLAGS_COLUMN);
   1556             mContentBytes = cursor.getBlob(CONTENT_CONTENT_BYTES_COLUMN);
   1557             mAccountKey = cursor.getLong(CONTENT_ACCOUNT_KEY_COLUMN);
   1558             mUiState = cursor.getInt(CONTENT_UI_STATE_COLUMN);
   1559             mUiDestination = cursor.getInt(CONTENT_UI_DESTINATION_COLUMN);
   1560             mUiDownloadedSize = cursor.getInt(CONTENT_UI_DOWNLOADED_SIZE_COLUMN);
   1561         }
   1562 
   1563         @Override
   1564         public ContentValues toContentValues() {
   1565             ContentValues values = new ContentValues();
   1566             values.put(AttachmentColumns.FILENAME, mFileName);
   1567             values.put(AttachmentColumns.MIME_TYPE, mMimeType);
   1568             values.put(AttachmentColumns.SIZE, mSize);
   1569             values.put(AttachmentColumns.CONTENT_ID, mContentId);
   1570             values.put(AttachmentColumns.CONTENT_URI, mContentUri);
   1571             values.put(AttachmentColumns.CACHED_FILE, mCachedFileUri);
   1572             values.put(AttachmentColumns.MESSAGE_KEY, mMessageKey);
   1573             values.put(AttachmentColumns.LOCATION, mLocation);
   1574             values.put(AttachmentColumns.ENCODING, mEncoding);
   1575             values.put(AttachmentColumns.CONTENT, mContent);
   1576             values.put(AttachmentColumns.FLAGS, mFlags);
   1577             values.put(AttachmentColumns.CONTENT_BYTES, mContentBytes);
   1578             values.put(AttachmentColumns.ACCOUNT_KEY, mAccountKey);
   1579             values.put(AttachmentColumns.UI_STATE, mUiState);
   1580             values.put(AttachmentColumns.UI_DESTINATION, mUiDestination);
   1581             values.put(AttachmentColumns.UI_DOWNLOADED_SIZE, mUiDownloadedSize);
   1582             return values;
   1583         }
   1584 
   1585         @Override
   1586         public int describeContents() {
   1587              return 0;
   1588         }
   1589 
   1590         @Override
   1591         public void writeToParcel(Parcel dest, int flags) {
   1592             // mBaseUri is not parceled
   1593             dest.writeLong(mId);
   1594             dest.writeString(mFileName);
   1595             dest.writeString(mMimeType);
   1596             dest.writeLong(mSize);
   1597             dest.writeString(mContentId);
   1598             dest.writeString(mContentUri);
   1599             dest.writeString(mCachedFileUri);
   1600             dest.writeLong(mMessageKey);
   1601             dest.writeString(mLocation);
   1602             dest.writeString(mEncoding);
   1603             dest.writeString(mContent);
   1604             dest.writeInt(mFlags);
   1605             dest.writeLong(mAccountKey);
   1606             if (mContentBytes == null) {
   1607                 dest.writeInt(-1);
   1608             } else {
   1609                 dest.writeInt(mContentBytes.length);
   1610                 dest.writeByteArray(mContentBytes);
   1611             }
   1612             dest.writeInt(mUiState);
   1613             dest.writeInt(mUiDestination);
   1614             dest.writeInt(mUiDownloadedSize);
   1615         }
   1616 
   1617         public Attachment(Parcel in) {
   1618             mBaseUri = Attachment.CONTENT_URI;
   1619             mId = in.readLong();
   1620             mFileName = in.readString();
   1621             mMimeType = in.readString();
   1622             mSize = in.readLong();
   1623             mContentId = in.readString();
   1624             mContentUri = in.readString();
   1625             mCachedFileUri = in.readString();
   1626             mMessageKey = in.readLong();
   1627             mLocation = in.readString();
   1628             mEncoding = in.readString();
   1629             mContent = in.readString();
   1630             mFlags = in.readInt();
   1631             mAccountKey = in.readLong();
   1632             final int contentBytesLen = in.readInt();
   1633             if (contentBytesLen == -1) {
   1634                 mContentBytes = null;
   1635             } else {
   1636                 mContentBytes = new byte[contentBytesLen];
   1637                 in.readByteArray(mContentBytes);
   1638             }
   1639             mUiState = in.readInt();
   1640             mUiDestination = in.readInt();
   1641             mUiDownloadedSize = in.readInt();
   1642          }
   1643 
   1644         public static final Parcelable.Creator<EmailContent.Attachment> CREATOR
   1645                 = new Parcelable.Creator<EmailContent.Attachment>() {
   1646             @Override
   1647             public EmailContent.Attachment createFromParcel(Parcel in) {
   1648                 return new EmailContent.Attachment(in);
   1649             }
   1650 
   1651             @Override
   1652             public EmailContent.Attachment[] newArray(int size) {
   1653                 return new EmailContent.Attachment[size];
   1654             }
   1655         };
   1656 
   1657         @Override
   1658         public String toString() {
   1659             return "[" + mFileName + ", " + mMimeType + ", " + mSize + ", " + mContentId + ", "
   1660                     + mContentUri + ", " + mCachedFileUri + ", " + mMessageKey + ", "
   1661                     + mLocation + ", " + mEncoding  + ", " + mFlags + ", " + mContentBytes + ", "
   1662                     + mAccountKey +  "," + mUiState + "," + mUiDestination + ","
   1663                     + mUiDownloadedSize + "]";
   1664         }
   1665     }
   1666 
   1667     public interface AccountColumns extends BaseColumns {
   1668         // The display name of the account (user-settable)
   1669         public static final String DISPLAY_NAME = "displayName";
   1670         // The email address corresponding to this account
   1671         public static final String EMAIL_ADDRESS = "emailAddress";
   1672         // A server-based sync key on an account-wide basis (EAS needs this)
   1673         public static final String SYNC_KEY = "syncKey";
   1674         // The default sync lookback period for this account
   1675         public static final String SYNC_LOOKBACK = "syncLookback";
   1676         // The default sync frequency for this account, in minutes
   1677         public static final String SYNC_INTERVAL = "syncInterval";
   1678         // A foreign key into the account manager, having host, login, password, port, and ssl flags
   1679         public static final String HOST_AUTH_KEY_RECV = "hostAuthKeyRecv";
   1680         // (optional) A foreign key into the account manager, having host, login, password, port,
   1681         // and ssl flags
   1682         public static final String HOST_AUTH_KEY_SEND = "hostAuthKeySend";
   1683         // Flags
   1684         public static final String FLAGS = "flags";
   1685         /**
   1686          * Default account
   1687          *
   1688          * @deprecated This should never be used any more, as default accounts are handled
   1689          *             differently now
   1690          */
   1691         @Deprecated
   1692         public static final String IS_DEFAULT = "isDefault";
   1693         // Old-Style UUID for compatibility with previous versions
   1694         @Deprecated
   1695         public static final String COMPATIBILITY_UUID = "compatibilityUuid";
   1696         // User name (for outgoing messages)
   1697         public static final String SENDER_NAME = "senderName";
   1698         /**
   1699          * Ringtone
   1700          *
   1701          * @deprecated Only used for creating the database (legacy reasons) and migration.
   1702          */
   1703         @Deprecated
   1704         public static final String RINGTONE_URI = "ringtoneUri";
   1705         // Protocol version (arbitrary string, used by EAS currently)
   1706         public static final String PROTOCOL_VERSION = "protocolVersion";
   1707         // The number of new messages (reported by the sync/download engines
   1708         @Deprecated
   1709         public static final String NEW_MESSAGE_COUNT = "newMessageCount";
   1710         // Legacy flags defining security (provisioning) requirements of this account; this
   1711         // information is now found in the Policy table; POLICY_KEY (below) is the foreign key
   1712         @Deprecated
   1713         public static final String SECURITY_FLAGS = "securityFlags";
   1714         // Server-based sync key for the security policies currently enforced
   1715         public static final String SECURITY_SYNC_KEY = "securitySyncKey";
   1716         // Signature to use with this account
   1717         public static final String SIGNATURE = "signature";
   1718         // A foreign key into the Policy table
   1719         public static final String POLICY_KEY = "policyKey";
   1720         // Max upload attachment size.
   1721         public static final String MAX_ATTACHMENT_SIZE = "maxAttachmentSize";
   1722         // Current duration of the Exchange ping
   1723         public static final String PING_DURATION = "pingDuration";
   1724     }
   1725 
   1726     public interface QuickResponseColumns extends BaseColumns {
   1727         // The QuickResponse text
   1728         static final String TEXT = "quickResponse";
   1729         // A foreign key into the Account table owning the QuickResponse
   1730         static final String ACCOUNT_KEY = "accountKey";
   1731     }
   1732 
   1733     public interface MailboxColumns extends BaseColumns {
   1734         // Use _ID instead
   1735         @Deprecated
   1736         public static final String ID = "_id";
   1737         // The display name of this mailbox [INDEX]
   1738         static final String DISPLAY_NAME = "displayName";
   1739         // The server's identifier for this mailbox
   1740         public static final String SERVER_ID = "serverId";
   1741         // The server's identifier for the parent of this mailbox (null = top-level)
   1742         public static final String PARENT_SERVER_ID = "parentServerId";
   1743         // A foreign key for the parent of this mailbox (-1 = top-level, 0=uninitialized)
   1744         public static final String PARENT_KEY = "parentKey";
   1745         // A foreign key to the Account that owns this mailbox
   1746         public static final String ACCOUNT_KEY = "accountKey";
   1747         // The type (role) of this mailbox
   1748         public static final String TYPE = "type";
   1749         // The hierarchy separator character
   1750         public static final String DELIMITER = "delimiter";
   1751         // Server-based sync key or validity marker (e.g. "SyncKey" for EAS, "uidvalidity" for IMAP)
   1752         public static final String SYNC_KEY = "syncKey";
   1753         // The sync lookback period for this mailbox (or null if using the account default)
   1754         public static final String SYNC_LOOKBACK = "syncLookback";
   1755         // The sync frequency for this mailbox (or null if using the account default)
   1756         public static final String SYNC_INTERVAL = "syncInterval";
   1757         // The time of last successful sync completion (millis)
   1758         public static final String SYNC_TIME = "syncTime";
   1759         // Cached unread count
   1760         public static final String UNREAD_COUNT = "unreadCount";
   1761         // Visibility of this folder in a list of folders [INDEX]
   1762         public static final String FLAG_VISIBLE = "flagVisible";
   1763         // Other states, as a bit field, e.g. CHILDREN_VISIBLE, HAS_CHILDREN
   1764         public static final String FLAGS = "flags";
   1765         // Backward compatible
   1766         @Deprecated
   1767         public static final String VISIBLE_LIMIT = "visibleLimit";
   1768         // Sync status (can be used as desired by sync services)
   1769         public static final String SYNC_STATUS = "syncStatus";
   1770         // Number of messages locally available in the mailbox.
   1771         public static final String MESSAGE_COUNT = "messageCount";
   1772         // The last time a message in this mailbox has been read (in millis)
   1773         public static final String LAST_TOUCHED_TIME = "lastTouchedTime";
   1774         // The UIProvider sync status
   1775         public static final String UI_SYNC_STATUS = "uiSyncStatus";
   1776         // The UIProvider last sync result
   1777         public static final String UI_LAST_SYNC_RESULT = "uiLastSyncResult";
   1778         /**
   1779          * The UIProvider sync status
   1780          *
   1781          * @deprecated This is no longer used by anything except for creating the database.
   1782          */
   1783         @Deprecated
   1784         public static final String LAST_NOTIFIED_MESSAGE_KEY = "lastNotifiedMessageKey";
   1785         /**
   1786          * The UIProvider last sync result
   1787         *
   1788         * @deprecated This is no longer used by anything except for creating the database.
   1789         */
   1790        @Deprecated
   1791         public static final String LAST_NOTIFIED_MESSAGE_COUNT = "lastNotifiedMessageCount";
   1792         // The total number of messages in the remote mailbox
   1793         public static final String TOTAL_COUNT = "totalCount";
   1794         // The full hierarchical name of this folder, in the form a/b/c
   1795         public static final String HIERARCHICAL_NAME = "hierarchicalName";
   1796         // The last time that we did a full sync. Set from SystemClock.elapsedRealtime().
   1797         public static final String LAST_FULL_SYNC_TIME = "lastFullSyncTime";
   1798     }
   1799 
   1800     public interface HostAuthColumns extends BaseColumns {
   1801         // The protocol (e.g. "imap", "pop3", "eas", "smtp"
   1802         static final String PROTOCOL = "protocol";
   1803         // The host address
   1804         static final String ADDRESS = "address";
   1805         // The port to use for the connection
   1806         static final String PORT = "port";
   1807         // General purpose flags
   1808         static final String FLAGS = "flags";
   1809         // The login (user name)
   1810         static final String LOGIN = "login";
   1811         // Password
   1812         static final String PASSWORD = "password";
   1813         // A domain or path, if required (used in IMAP and EAS)
   1814         static final String DOMAIN = "domain";
   1815         // An alias to a local client certificate for SSL
   1816         static final String CLIENT_CERT_ALIAS = "certAlias";
   1817         // DEPRECATED - Will not be set or stored
   1818         static final String ACCOUNT_KEY = "accountKey";
   1819         // A blob containing an X509 server certificate
   1820         static final String SERVER_CERT = "serverCert";
   1821         // The credentials row this hostAuth should use. Currently only set if using OAuth.
   1822         static final String CREDENTIAL_KEY = "credentialKey";
   1823     }
   1824 
   1825     public interface PolicyColumns extends BaseColumns {
   1826         public static final String PASSWORD_MODE = "passwordMode";
   1827         public static final String PASSWORD_MIN_LENGTH = "passwordMinLength";
   1828         public static final String PASSWORD_EXPIRATION_DAYS = "passwordExpirationDays";
   1829         public static final String PASSWORD_HISTORY = "passwordHistory";
   1830         public static final String PASSWORD_COMPLEX_CHARS = "passwordComplexChars";
   1831         public static final String PASSWORD_MAX_FAILS = "passwordMaxFails";
   1832         public static final String MAX_SCREEN_LOCK_TIME = "maxScreenLockTime";
   1833         public static final String REQUIRE_REMOTE_WIPE = "requireRemoteWipe";
   1834         public static final String REQUIRE_ENCRYPTION = "requireEncryption";
   1835         public static final String REQUIRE_ENCRYPTION_EXTERNAL = "requireEncryptionExternal";
   1836         // ICS additions
   1837         // Note: the appearance of these columns does not imply that we support these features; only
   1838         // that we store them in the Policy structure
   1839         public static final String REQUIRE_MANUAL_SYNC_WHEN_ROAMING = "requireManualSyncRoaming";
   1840         public static final String DONT_ALLOW_CAMERA = "dontAllowCamera";
   1841         public static final String DONT_ALLOW_ATTACHMENTS = "dontAllowAttachments";
   1842         public static final String DONT_ALLOW_HTML = "dontAllowHtml";
   1843         public static final String MAX_ATTACHMENT_SIZE = "maxAttachmentSize";
   1844         public static final String MAX_TEXT_TRUNCATION_SIZE = "maxTextTruncationSize";
   1845         public static final String MAX_HTML_TRUNCATION_SIZE = "maxHTMLTruncationSize";
   1846         public static final String MAX_EMAIL_LOOKBACK = "maxEmailLookback";
   1847         public static final String MAX_CALENDAR_LOOKBACK = "maxCalendarLookback";
   1848         // Indicates that the server allows password recovery, not that we support it
   1849         public static final String PASSWORD_RECOVERY_ENABLED = "passwordRecoveryEnabled";
   1850         // Tokenized strings indicating protocol specific policies enforced/unsupported
   1851         public static final String PROTOCOL_POLICIES_ENFORCED = "protocolPoliciesEnforced";
   1852         public static final String PROTOCOL_POLICIES_UNSUPPORTED = "protocolPoliciesUnsupported";
   1853     }
   1854 }
   1855