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 
18 package com.android.emailcommon.provider;
19 
20 import android.content.ContentProviderOperation;
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.database.Cursor;
27 import android.net.Uri;
28 import android.os.Bundle;
29 import android.os.Parcel;
30 import android.os.Parcelable;
31 import android.os.RemoteException;
32 import android.provider.CalendarContract;
33 import android.provider.ContactsContract;
34 import android.text.TextUtils;
35 import android.util.SparseBooleanArray;
36 
37 import com.android.emailcommon.Logging;
38 import com.android.emailcommon.R;
39 import com.android.emailcommon.utility.Utility;
40 import com.android.mail.utils.LogUtils;
41 
42 import java.util.ArrayList;
43 
44 public class Mailbox extends EmailContent implements EmailContent.MailboxColumns, Parcelable {
45     /**
46      * Sync extras key when syncing one or more mailboxes to specify how many
47      * mailboxes are included in the extra.
48      */
49     public static final String SYNC_EXTRA_MAILBOX_COUNT = "__mailboxCount__";
50     /**
51      * Sync extras key pattern when syncing one or more mailboxes to specify
52      * which mailbox to sync. Is intentionally private, we have helper functions
53      * to set up an appropriate bundle, or read its contents.
54      */
55     private static final String SYNC_EXTRA_MAILBOX_ID_PATTERN = "__mailboxId%d__";
56     /**
57      * Sync extra key indicating that we are doing a sync of the folder structure for an account.
58      */
59     public static final String SYNC_EXTRA_ACCOUNT_ONLY = "__account_only__";
60     /**
61      * Sync extra key indicating that we are only starting a ping.
62      */
63     public static final String SYNC_EXTRA_PUSH_ONLY = "__push_only__";
64 
65     /**
66      * Sync extras key to specify that only a specific mailbox type should be synced.
67      */
68     public static final String SYNC_EXTRA_MAILBOX_TYPE = "__mailboxType__";
69     /**
70      * Sync extras key when syncing a mailbox to specify how many additional messages to sync.
71      */
72     public static final String SYNC_EXTRA_DELTA_MESSAGE_COUNT = "__deltaMessageCount__";
73 
74     public static final String SYNC_EXTRA_NOOP = "__noop__";
75 
76     public static final String TABLE_NAME = "Mailbox";
77 
78 
79     public static Uri CONTENT_URI;
80     public static Uri MESSAGE_COUNT_URI;
81 
initMailbox()82     public static void initMailbox() {
83         CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/mailbox");
84         MESSAGE_COUNT_URI = Uri.parse(EmailContent.CONTENT_URI + "/mailboxCount");
85     }
86 
formatMailboxIdExtra(final int index)87     private static String formatMailboxIdExtra(final int index) {
88         return String.format(SYNC_EXTRA_MAILBOX_ID_PATTERN, index);
89     }
90 
createSyncBundle(final ArrayList<Long> mailboxIds)91     public static Bundle createSyncBundle(final ArrayList<Long> mailboxIds) {
92         final Bundle bundle = new Bundle(mailboxIds.size() + 1);
93         bundle.putInt(SYNC_EXTRA_MAILBOX_COUNT, mailboxIds.size());
94         for (int i = 0; i < mailboxIds.size(); i++) {
95             bundle.putLong(formatMailboxIdExtra(i), mailboxIds.get(i));
96         }
97         return bundle;
98     }
99 
createSyncBundle(final long[] mailboxIds)100     public static Bundle createSyncBundle(final long[] mailboxIds) {
101         final Bundle bundle = new Bundle(mailboxIds.length + 1);
102         bundle.putInt(SYNC_EXTRA_MAILBOX_COUNT, mailboxIds.length);
103         for (int i = 0; i < mailboxIds.length; i++) {
104             bundle.putLong(formatMailboxIdExtra(i), mailboxIds[i]);
105         }
106         return bundle;
107     }
108 
createSyncBundle(final long mailboxId)109     public static Bundle createSyncBundle(final long mailboxId) {
110         final Bundle bundle = new Bundle(2);
111         bundle.putInt(SYNC_EXTRA_MAILBOX_COUNT, 1);
112         bundle.putLong(formatMailboxIdExtra(0), mailboxId);
113         return bundle;
114     }
115 
getMailboxIdsFromBundle(Bundle bundle)116     public static long[] getMailboxIdsFromBundle(Bundle bundle) {
117         final int count = bundle.getInt(SYNC_EXTRA_MAILBOX_COUNT, 0);
118         if (count > 0) {
119             if (bundle.getBoolean(SYNC_EXTRA_PUSH_ONLY, false)) {
120                 LogUtils.w(Logging.LOG_TAG, "Mailboxes specified in a push only sync");
121             }
122             if (bundle.getBoolean(SYNC_EXTRA_ACCOUNT_ONLY, false)) {
123                 LogUtils.w(Logging.LOG_TAG, "Mailboxes specified in an account only sync");
124             }
125             final long [] result = new long[count];
126             for (int i = 0; i < count; i++) {
127                 result[i] = bundle.getLong(formatMailboxIdExtra(i), 0);
128             }
129 
130             return result;
131         } else {
132             return null;
133         }
134     }
135 
isAccountOnlyExtras(Bundle bundle)136     public static boolean isAccountOnlyExtras(Bundle bundle) {
137         final boolean result = bundle.getBoolean(SYNC_EXTRA_ACCOUNT_ONLY, false);
138         if (result) {
139             final int count = bundle.getInt(SYNC_EXTRA_MAILBOX_COUNT, 0);
140             if (count != 0) {
141                 LogUtils.w(Logging.LOG_TAG, "Mailboxes specified in an account only sync");
142             }
143         }
144         return result;
145     }
146 
isPushOnlyExtras(Bundle bundle)147     public static boolean isPushOnlyExtras(Bundle bundle) {
148         final boolean result = bundle.getBoolean(SYNC_EXTRA_PUSH_ONLY, false);
149         if (result) {
150             final int count = bundle.getInt(SYNC_EXTRA_MAILBOX_COUNT, 0);
151             if (count != 0) {
152                 LogUtils.w(Logging.LOG_TAG, "Mailboxes specified in a push only sync");
153             }
154         }
155         return result;
156     }
157 
158     public String mDisplayName;
159     public String mServerId;
160     public String mParentServerId;
161     public long mParentKey;
162     public long mAccountKey;
163     public int mType;
164     public int mDelimiter;
165     public String mSyncKey;
166     public int mSyncLookback;
167     public int mSyncInterval;
168     public long mSyncTime;
169     public boolean mFlagVisible = true;
170     public int mFlags;
171     public String mSyncStatus;
172     public long mLastTouchedTime;
173     public int mUiSyncStatus;
174     public int mUiLastSyncResult;
175     public int mTotalCount;
176     public String mHierarchicalName;
177     public long mLastFullSyncTime;
178 
179     public static final int CONTENT_ID_COLUMN = 0;
180     public static final int CONTENT_DISPLAY_NAME_COLUMN = 1;
181     public static final int CONTENT_SERVER_ID_COLUMN = 2;
182     public static final int CONTENT_PARENT_SERVER_ID_COLUMN = 3;
183     public static final int CONTENT_ACCOUNT_KEY_COLUMN = 4;
184     public static final int CONTENT_TYPE_COLUMN = 5;
185     public static final int CONTENT_DELIMITER_COLUMN = 6;
186     public static final int CONTENT_SYNC_KEY_COLUMN = 7;
187     public static final int CONTENT_SYNC_LOOKBACK_COLUMN = 8;
188     public static final int CONTENT_SYNC_INTERVAL_COLUMN = 9;
189     public static final int CONTENT_SYNC_TIME_COLUMN = 10;
190     public static final int CONTENT_FLAG_VISIBLE_COLUMN = 11;
191     public static final int CONTENT_FLAGS_COLUMN = 12;
192     public static final int CONTENT_SYNC_STATUS_COLUMN = 13;
193     public static final int CONTENT_PARENT_KEY_COLUMN = 14;
194     public static final int CONTENT_LAST_TOUCHED_TIME_COLUMN = 15;
195     public static final int CONTENT_UI_SYNC_STATUS_COLUMN = 16;
196     public static final int CONTENT_UI_LAST_SYNC_RESULT_COLUMN = 17;
197     public static final int CONTENT_TOTAL_COUNT_COLUMN = 18;
198     public static final int CONTENT_HIERARCHICAL_NAME_COLUMN = 19;
199     public static final int CONTENT_LAST_FULL_SYNC_COLUMN = 20;
200 
201     /**
202      * <em>NOTE</em>: If fields are added or removed, the method {@link #getHashes()}
203      * MUST be updated.
204      */
205     public static final String[] CONTENT_PROJECTION = new String[] {
206             MailboxColumns._ID,
207             MailboxColumns.DISPLAY_NAME,
208             MailboxColumns.SERVER_ID,
209             MailboxColumns.PARENT_SERVER_ID,
210             MailboxColumns.ACCOUNT_KEY,
211             MailboxColumns.TYPE,
212             MailboxColumns.DELIMITER,
213             MailboxColumns.SYNC_KEY,
214             MailboxColumns.SYNC_LOOKBACK,
215             MailboxColumns.SYNC_INTERVAL,
216             MailboxColumns.SYNC_TIME,
217             MailboxColumns.FLAG_VISIBLE,
218             MailboxColumns.FLAGS,
219             MailboxColumns.SYNC_STATUS,
220             MailboxColumns.PARENT_KEY,
221             MailboxColumns.LAST_TOUCHED_TIME,
222             MailboxColumns.UI_SYNC_STATUS,
223             MailboxColumns.UI_LAST_SYNC_RESULT,
224             MailboxColumns.TOTAL_COUNT,
225             MailboxColumns.HIERARCHICAL_NAME,
226             MailboxColumns.LAST_FULL_SYNC_TIME
227     };
228 
229     /** Selection by server pathname for a given account */
230     public static final String PATH_AND_ACCOUNT_SELECTION =
231         MailboxColumns.SERVER_ID + "=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
232 
233     private static final String[] MAILBOX_TYPE_PROJECTION = new String [] {
234             MailboxColumns.TYPE
235     };
236     private static final int MAILBOX_TYPE_TYPE_COLUMN = 0;
237 
238     private static final String[] MAILBOX_DISPLAY_NAME_PROJECTION = new String [] {
239             MailboxColumns.DISPLAY_NAME
240     };
241     private static final int MAILBOX_DISPLAY_NAME_COLUMN = 0;
242 
243     /**
244      * Projection to use when reading {@link MailboxColumns#ACCOUNT_KEY} for a mailbox.
245      */
246     private static final String[] ACCOUNT_KEY_PROJECTION = { MailboxColumns.ACCOUNT_KEY };
247     private static final int ACCOUNT_KEY_PROJECTION_ACCOUNT_KEY_COLUMN = 0;
248 
249     /**
250      * Projection for querying data needed during a sync.
251      */
252     public interface ProjectionSyncData {
253         public static final int COLUMN_SERVER_ID = 0;
254         public static final int COLUMN_SYNC_KEY = 1;
255 
256         public static final String[] PROJECTION = {
257                 MailboxColumns.SERVER_ID,
258                 MailboxColumns.SYNC_KEY
259         };
260     }
261 
262     public static final long NO_MAILBOX = -1;
263 
264     // Sentinel values for the mSyncInterval field of both Mailbox records
265     @Deprecated
266     public static final int CHECK_INTERVAL_NEVER = -1;
267     @Deprecated
268     public static final int CHECK_INTERVAL_PUSH = -2;
269     // The following two sentinel values are used by EAS
270     // Ping indicates that the EAS mailbox is synced based on a "ping" from the server
271     @Deprecated
272     public static final int CHECK_INTERVAL_PING = -3;
273     // Push-Hold indicates an EAS push or ping Mailbox shouldn't sync just yet
274     @Deprecated
275     public static final int CHECK_INTERVAL_PUSH_HOLD = -4;
276 
277     // Sentinel for PARENT_KEY.  Use NO_MAILBOX for toplevel mailboxes (i.e. no parents).
278     public static final long PARENT_KEY_UNINITIALIZED = 0L;
279 
280     private static final String WHERE_TYPE_AND_ACCOUNT_KEY =
281         MailboxColumns.TYPE + "=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
282 
283     /**
284      * Selection for mailboxes that should receive push for an account. A mailbox should receive
285      * push if it has a valid, non-initial sync key and is opted in for sync.
286      */
287     private static final String PUSH_MAILBOXES_FOR_ACCOUNT_SELECTION =
288             MailboxColumns.SYNC_KEY + " is not null and " + MailboxColumns.SYNC_KEY + "!='' and " +
289                     MailboxColumns.SYNC_KEY + "!='0' and " + MailboxColumns.SYNC_INTERVAL +
290                     "=1 and " + MailboxColumns.ACCOUNT_KEY + "=?";
291 
292     /** Selection for mailboxes that say they want to sync, plus outbox, for an account. */
293     private static final String OUTBOX_PLUS_SYNCING_AND_ACCOUNT_SELECTION = "("
294             + MailboxColumns.TYPE + "=" + Mailbox.TYPE_OUTBOX + " or "
295             + MailboxColumns.SYNC_INTERVAL + "=1) and " + MailboxColumns.ACCOUNT_KEY + "=?";
296 
297     /** Selection for mailboxes that are configured for sync of a certain type for an account. */
298     private static final String SYNCING_AND_TYPE_FOR_ACCOUNT_SELECTION =
299             MailboxColumns.SYNC_INTERVAL + "=1 and " + MailboxColumns.TYPE + "=? and " +
300                     MailboxColumns.ACCOUNT_KEY + "=?";
301 
302     // Types of mailboxes.  The list is ordered to match a typical UI presentation, e.g.
303     // placing the inbox at the top.
304     // Arrays of "special_mailbox_display_names" and "special_mailbox_icons" are depends on
305     // types Id of mailboxes.
306     /** No type specified */
307     public static final int TYPE_NONE = -1;
308     /** The "main" mailbox for the account, almost always referred to as "Inbox" */
309     public static final int TYPE_INBOX = 0;
310     // Types of mailboxes
311     /** Generic mailbox that holds mail */
312     public static final int TYPE_MAIL = 1;
313     /** Parent-only mailbox; does not hold any mail */
314     public static final int TYPE_PARENT = 2;
315     /** Drafts mailbox */
316     public static final int TYPE_DRAFTS = 3;
317     /** Local mailbox associated with the account's outgoing mail */
318     public static final int TYPE_OUTBOX = 4;
319     /** Sent mail; mail that was sent from the account */
320     public static final int TYPE_SENT = 5;
321     /** Deleted mail */
322     public static final int TYPE_TRASH = 6;
323     /** Junk mail */
324     public static final int TYPE_JUNK = 7;
325     /** Search results */
326     public static final int TYPE_SEARCH = 8;
327     /** Starred (virtual) */
328     public static final int TYPE_STARRED = 9;
329     /** All unread mail (virtual) */
330     public static final int TYPE_UNREAD = 10;
331 
332     // Types after this are used for non-mail mailboxes (as in EAS)
333     public static final int TYPE_NOT_EMAIL = 0x40;
334     public static final int TYPE_CALENDAR = 0x41;
335     public static final int TYPE_CONTACTS = 0x42;
336     public static final int TYPE_TASKS = 0x43;
337     @Deprecated
338     public static final int TYPE_EAS_ACCOUNT_MAILBOX = 0x44;
339     public static final int TYPE_UNKNOWN = 0x45;
340 
341     /**
342      * Specifies which mailbox types may be synced from server, and what the default sync interval
343      * value should be.
344      * If a mailbox type is in this array, then it can be synced.
345      * If the mailbox type is mapped to true in this array, then new mailboxes of that type should
346      * be set to automatically sync (either with the periodic poll, or with push, as determined
347      * by the account's sync settings).
348      * See {@link #isSyncableType} and {@link #getDefaultSyncStateForType} for how to access this
349      * data.
350      */
351     private static final SparseBooleanArray SYNCABLE_TYPES;
352     static {
353         SYNCABLE_TYPES = new SparseBooleanArray(7);
SYNCABLE_TYPES.put(TYPE_INBOX, true)354         SYNCABLE_TYPES.put(TYPE_INBOX, true);
SYNCABLE_TYPES.put(TYPE_MAIL, false)355         SYNCABLE_TYPES.put(TYPE_MAIL, false);
356         // TODO: b/11158759
357         // For now, drafts folders are not syncable.
358         //SYNCABLE_TYPES.put(TYPE_DRAFTS, true);
SYNCABLE_TYPES.put(TYPE_SENT, true)359         SYNCABLE_TYPES.put(TYPE_SENT, true);
SYNCABLE_TYPES.put(TYPE_TRASH, false)360         SYNCABLE_TYPES.put(TYPE_TRASH, false);
SYNCABLE_TYPES.put(TYPE_CALENDAR, true)361         SYNCABLE_TYPES.put(TYPE_CALENDAR, true);
SYNCABLE_TYPES.put(TYPE_CONTACTS, true)362         SYNCABLE_TYPES.put(TYPE_CONTACTS, true);
363     }
364 
365     public static final int TYPE_NOT_SYNCABLE = 0x100;
366     // A mailbox that holds Messages that are attachments
367     public static final int TYPE_ATTACHMENT = 0x101;
368 
369     /**
370      * For each of the following folder types, we expect there to be exactly one folder of that
371      * type per account.
372      * Each sync adapter must do the following:
373      * 1) On initial sync: For each type that was not found from the server, create a local folder.
374      * 2) On folder delete: If it's of a required type, convert it to local rather than delete.
375      * 3) On folder add: If it's of a required type, convert the local folder to server.
376      * 4) When adding a duplicate (either initial sync or folder add): Error.
377      */
378     public static final int[] REQUIRED_FOLDER_TYPES =
379             { TYPE_INBOX, TYPE_DRAFTS, TYPE_OUTBOX, TYPE_SENT, TYPE_TRASH };
380 
381     // Default "touch" time for system mailboxes
382     public static final int DRAFTS_DEFAULT_TOUCH_TIME = 2;
383     public static final int SENT_DEFAULT_TOUCH_TIME = 1;
384 
385     // Bit field flags; each is defined below
386     // Warning: Do not read these flags until POP/IMAP/EAS all populate them
387     /** No flags set */
388     public static final int FLAG_NONE = 0;
389     /** Has children in the mailbox hierarchy */
390     public static final int FLAG_HAS_CHILDREN = 1<<0;
391     /** Children are visible in the UI */
392     public static final int FLAG_CHILDREN_VISIBLE = 1<<1;
393     /** cannot receive "pushed" mail */
394     public static final int FLAG_CANT_PUSH = 1<<2;
395     /** can hold emails (i.e. some parent mailboxes cannot themselves contain mail) */
396     public static final int FLAG_HOLDS_MAIL = 1<<3;
397     /** can be used as a target for moving messages within the account */
398     public static final int FLAG_ACCEPTS_MOVED_MAIL = 1<<4;
399     /** can be used as a target for appending messages */
400     public static final int FLAG_ACCEPTS_APPENDED_MAIL = 1<<5;
401     /** has user settings (sync lookback, etc.) */
402     public static final int FLAG_SUPPORTS_SETTINGS = 1<<6;
403 
404     // Magic mailbox ID's
405     // NOTE:  This is a quick solution for merged mailboxes.  I would rather implement this
406     // with a more generic way of packaging and sharing queries between activities
407     public static final long QUERY_ALL_INBOXES = -2;
408     public static final long QUERY_ALL_UNREAD = -3;
409     public static final long QUERY_ALL_FAVORITES = -4;
410     public static final long QUERY_ALL_DRAFTS = -5;
411     public static final long QUERY_ALL_OUTBOX = -6;
412 
Mailbox()413     public Mailbox() {
414         mBaseUri = CONTENT_URI;
415     }
416 
getSystemMailboxName(Context context, int mailboxType)417     public static String getSystemMailboxName(Context context, int mailboxType) {
418         final int resId;
419         switch (mailboxType) {
420             case Mailbox.TYPE_INBOX:
421                 resId = R.string.mailbox_name_server_inbox;
422                 break;
423             case Mailbox.TYPE_OUTBOX:
424                 resId = R.string.mailbox_name_server_outbox;
425                 break;
426             case Mailbox.TYPE_DRAFTS:
427                 resId = R.string.mailbox_name_server_drafts;
428                 break;
429             case Mailbox.TYPE_TRASH:
430                 resId = R.string.mailbox_name_server_trash;
431                 break;
432             case Mailbox.TYPE_SENT:
433                 resId = R.string.mailbox_name_server_sent;
434                 break;
435             case Mailbox.TYPE_JUNK:
436                 resId = R.string.mailbox_name_server_junk;
437                 break;
438             case Mailbox.TYPE_STARRED:
439                 resId = R.string.mailbox_name_server_starred;
440                 break;
441             case Mailbox.TYPE_UNREAD:
442                 resId = R.string.mailbox_name_server_all_unread;
443                 break;
444             default:
445                 throw new IllegalArgumentException("Illegal mailbox type");
446         }
447         return context.getString(resId);
448     }
449 
450      /**
451      * Restore a Mailbox from the database, given its unique id
452      * @param context Makes provider calls
453      * @param id Row ID of mailbox to restore
454      * @return the instantiated Mailbox
455      */
restoreMailboxWithId(Context context, long id)456     public static Mailbox restoreMailboxWithId(Context context, long id) {
457         return EmailContent.restoreContentWithId(context, Mailbox.class,
458                 Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, id);
459     }
460 
461     /**
462      * Builds a new mailbox with "typical" settings for a system mailbox, such as a local "Drafts"
463      * mailbox. This is useful for protocols like POP3 or IMAP who don't have certain local
464      * system mailboxes synced with the server.
465      * Note: the mailbox is not persisted - clients must call {@link #save} themselves.
466      */
newSystemMailbox(Context context, long accountId, int mailboxType)467     public static Mailbox newSystemMailbox(Context context, long accountId, int mailboxType) {
468         // Sync interval and flags are different based on mailbox type.
469         // TODO: Sync interval doesn't seem to be used anywhere, make it matter or get rid of it.
470         final int syncInterval;
471         final int flags;
472         switch (mailboxType) {
473             case TYPE_INBOX:
474                 flags = Mailbox.FLAG_HOLDS_MAIL | Mailbox.FLAG_ACCEPTS_MOVED_MAIL;
475                 syncInterval = 0;
476                 break;
477             case TYPE_SENT:
478             case TYPE_TRASH:
479                 flags = Mailbox.FLAG_HOLDS_MAIL;
480                 syncInterval = 0;
481                 break;
482             case TYPE_DRAFTS:
483             case TYPE_OUTBOX:
484                 flags = Mailbox.FLAG_HOLDS_MAIL;
485                 syncInterval = Account.CHECK_INTERVAL_NEVER;
486                 break;
487             default:
488                 throw new IllegalArgumentException("Bad mailbox type for newSystemMailbox: " +
489                         mailboxType);
490         }
491 
492         final Mailbox box = new Mailbox();
493         box.mAccountKey = accountId;
494         box.mType = mailboxType;
495         box.mSyncInterval = syncInterval;
496         box.mFlagVisible = true;
497         // TODO: Fix how display names work.
498         box.mServerId = box.mDisplayName = getSystemMailboxName(context, mailboxType);
499         box.mParentKey = Mailbox.NO_MAILBOX;
500         box.mFlags = flags;
501         return box;
502     }
503 
504     /**
505      * Returns a Mailbox from the database, given its pathname and account id. All mailbox
506      * paths for a particular account must be unique. Paths are stored in the column
507      * {@link MailboxColumns#SERVER_ID} for want of yet another column in the table.
508      * @param context Makes provider calls
509      * @param accountId the ID of the account
510      * @param path the fully qualified, remote pathname
511      */
restoreMailboxForPath(Context context, long accountId, String path)512     public static Mailbox restoreMailboxForPath(Context context, long accountId, String path) {
513         final Cursor c = context.getContentResolver().query(
514                 Mailbox.CONTENT_URI,
515                 Mailbox.CONTENT_PROJECTION,
516                 Mailbox.PATH_AND_ACCOUNT_SELECTION,
517                 new String[] { path, Long.toString(accountId) },
518                 null);
519         if (c == null) throw new ProviderUnavailableException();
520         try {
521             Mailbox mailbox = null;
522             if (c.moveToFirst()) {
523                 mailbox = getContent(context, c, Mailbox.class);
524                 if (c.moveToNext()) {
525                     LogUtils.w(Logging.LOG_TAG, "Multiple mailboxes named \"%s\"", path);
526                 }
527             } else {
528                 LogUtils.i(Logging.LOG_TAG, "Could not find mailbox at \"%s\"", path);
529             }
530             return mailbox;
531         } finally {
532             c.close();
533         }
534     }
535 
536     /**
537      * Returns a {@link Mailbox} for the given path. If the path is not in the database, a new
538      * mailbox will be created.
539      */
getMailboxForPath(Context context, long accountId, String path)540     public static Mailbox getMailboxForPath(Context context, long accountId, String path) {
541         Mailbox mailbox = restoreMailboxForPath(context, accountId, path);
542         if (mailbox == null) {
543             mailbox = new Mailbox();
544         }
545         return mailbox;
546     }
547 
548     /**
549      * Check if a mailbox type can be synced with the server.
550      * @param mailboxType The type to check.
551      * @return Whether this type is syncable.
552      */
isSyncableType(final int mailboxType)553     public static boolean isSyncableType(final int mailboxType) {
554         return SYNCABLE_TYPES.indexOfKey(mailboxType) >= 0;
555     }
556 
557     /**
558      * Check if a mailbox type should sync with the server by default.
559      * @param mailboxType The type to check.
560      * @return Whether this type should default to syncing.
561      */
getDefaultSyncStateForType(final int mailboxType)562     public static boolean getDefaultSyncStateForType(final int mailboxType) {
563         return SYNCABLE_TYPES.get(mailboxType);
564     }
565 
566     /**
567      * Check whether this mailbox is syncable. It has to be both a server synced mailbox, and
568      * of a syncable able.
569      * @return Whether this mailbox is syncable.
570      */
isSyncable()571     public boolean isSyncable() {
572         return (mTotalCount >= 0) && isSyncableType(mType);
573     }
574 
575     @Override
restore(Cursor cursor)576     public void restore(Cursor cursor) {
577         mBaseUri = CONTENT_URI;
578         mId = cursor.getLong(CONTENT_ID_COLUMN);
579         mDisplayName = cursor.getString(CONTENT_DISPLAY_NAME_COLUMN);
580         mServerId = cursor.getString(CONTENT_SERVER_ID_COLUMN);
581         mParentServerId = cursor.getString(CONTENT_PARENT_SERVER_ID_COLUMN);
582         mParentKey = cursor.getLong(CONTENT_PARENT_KEY_COLUMN);
583         mAccountKey = cursor.getLong(CONTENT_ACCOUNT_KEY_COLUMN);
584         mType = cursor.getInt(CONTENT_TYPE_COLUMN);
585         mDelimiter = cursor.getInt(CONTENT_DELIMITER_COLUMN);
586         mSyncKey = cursor.getString(CONTENT_SYNC_KEY_COLUMN);
587         mSyncLookback = cursor.getInt(CONTENT_SYNC_LOOKBACK_COLUMN);
588         mSyncInterval = cursor.getInt(CONTENT_SYNC_INTERVAL_COLUMN);
589         mSyncTime = cursor.getLong(CONTENT_SYNC_TIME_COLUMN);
590         mFlagVisible = cursor.getInt(CONTENT_FLAG_VISIBLE_COLUMN) == 1;
591         mFlags = cursor.getInt(CONTENT_FLAGS_COLUMN);
592         mSyncStatus = cursor.getString(CONTENT_SYNC_STATUS_COLUMN);
593         mLastTouchedTime = cursor.getLong(CONTENT_LAST_TOUCHED_TIME_COLUMN);
594         mUiSyncStatus = cursor.getInt(CONTENT_UI_SYNC_STATUS_COLUMN);
595         mUiLastSyncResult = cursor.getInt(CONTENT_UI_LAST_SYNC_RESULT_COLUMN);
596         mTotalCount = cursor.getInt(CONTENT_TOTAL_COUNT_COLUMN);
597         mHierarchicalName = cursor.getString(CONTENT_HIERARCHICAL_NAME_COLUMN);
598         mLastFullSyncTime = cursor.getInt(CONTENT_LAST_FULL_SYNC_COLUMN);
599     }
600 
601     @Override
toContentValues()602     public ContentValues toContentValues() {
603         final ContentValues values = new ContentValues(20);
604         values.put(MailboxColumns.DISPLAY_NAME, mDisplayName);
605         values.put(MailboxColumns.SERVER_ID, mServerId);
606         values.put(MailboxColumns.PARENT_SERVER_ID, mParentServerId);
607         values.put(MailboxColumns.PARENT_KEY, mParentKey);
608         values.put(MailboxColumns.ACCOUNT_KEY, mAccountKey);
609         values.put(MailboxColumns.TYPE, mType);
610         values.put(MailboxColumns.DELIMITER, mDelimiter);
611         values.put(MailboxColumns.SYNC_KEY, mSyncKey);
612         values.put(MailboxColumns.SYNC_LOOKBACK, mSyncLookback);
613         values.put(MailboxColumns.SYNC_INTERVAL, mSyncInterval);
614         values.put(MailboxColumns.SYNC_TIME, mSyncTime);
615         values.put(MailboxColumns.FLAG_VISIBLE, mFlagVisible);
616         values.put(MailboxColumns.FLAGS, mFlags);
617         values.put(MailboxColumns.SYNC_STATUS, mSyncStatus);
618         values.put(MailboxColumns.LAST_TOUCHED_TIME, mLastTouchedTime);
619         values.put(MailboxColumns.UI_SYNC_STATUS, mUiSyncStatus);
620         values.put(MailboxColumns.UI_LAST_SYNC_RESULT, mUiLastSyncResult);
621         values.put(MailboxColumns.TOTAL_COUNT, mTotalCount);
622         values.put(MailboxColumns.HIERARCHICAL_NAME, mHierarchicalName);
623         values.put(MailboxColumns.LAST_FULL_SYNC_TIME, mLastFullSyncTime);
624         return values;
625     }
626 
627     /**
628      * Store the updated message count in the database.
629      * @param c Makes provider calls
630      * @param count New count
631      */
updateMessageCount(final Context c, final int count)632     public void updateMessageCount(final Context c, final int count) {
633         if (count != mTotalCount) {
634             final ContentValues values = new ContentValues(1);
635             values.put(MailboxColumns.TOTAL_COUNT, count);
636             update(c, values);
637             mTotalCount = count;
638         }
639     }
640 
641     /**
642      * Store the last full sync time in the database.
643      * @param c Makes provider calls
644      * @param syncTime New syncTime
645      */
updateLastFullSyncTime(final Context c, final long syncTime)646     public void updateLastFullSyncTime(final Context c, final long syncTime) {
647         if (syncTime != mLastFullSyncTime) {
648             final ContentValues values = new ContentValues(1);
649             values.put(MailboxColumns.LAST_FULL_SYNC_TIME, syncTime);
650             update(c, values);
651             mLastFullSyncTime = syncTime;
652         }
653     }
654 
655     /**
656      * Convenience method to return the id of a given type of Mailbox for a given Account; the
657      * common Mailbox types (Inbox, Outbox, Sent, Drafts, Trash, and Search) are all cached by
658      * EmailProvider; therefore, we warn if the mailbox is not found in the cache
659      *
660      * @param context the caller's context, used to get a ContentResolver
661      * @param accountId the id of the account to be queried
662      * @param type the mailbox type, as defined above
663      * @return the id of the mailbox, or -1 if not found
664      */
findMailboxOfType(Context context, long accountId, int type)665     public static long findMailboxOfType(Context context, long accountId, int type) {
666         final String[] bindArguments = new String[] {Long.toString(type), Long.toString(accountId)};
667         return Utility.getFirstRowLong(context, Mailbox.CONTENT_URI,
668                 ID_PROJECTION, WHERE_TYPE_AND_ACCOUNT_KEY, bindArguments, null,
669                 ID_PROJECTION_COLUMN, NO_MAILBOX);
670     }
671 
672     /**
673      * Convenience method that returns the mailbox found using the method above
674      */
restoreMailboxOfType(Context context, long accountId, int type)675     public static Mailbox restoreMailboxOfType(Context context, long accountId, int type) {
676         final long mailboxId = findMailboxOfType(context, accountId, type);
677         if (mailboxId != Mailbox.NO_MAILBOX) {
678             return Mailbox.restoreMailboxWithId(context, mailboxId);
679         }
680         return null;
681     }
682 
683     /**
684      * Return the mailbox for a message with a given id
685      * @param context the caller's context
686      * @param messageId the id of the message
687      * @return the mailbox, or null if the mailbox doesn't exist
688      */
getMailboxForMessageId(Context context, long messageId)689     public static Mailbox getMailboxForMessageId(Context context, long messageId) {
690         final long mailboxId = Message.getKeyColumnLong(context, messageId,
691                 MessageColumns.MAILBOX_KEY);
692         if (mailboxId != -1) {
693             return Mailbox.restoreMailboxWithId(context, mailboxId);
694         }
695         return null;
696     }
697 
698     /**
699      * @return mailbox type, or -1 if mailbox not found.
700      */
getMailboxType(Context context, long mailboxId)701     public static int getMailboxType(Context context, long mailboxId) {
702         final Uri url = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId);
703         return Utility.getFirstRowInt(context, url, MAILBOX_TYPE_PROJECTION,
704                 null, null, null, MAILBOX_TYPE_TYPE_COLUMN, -1);
705     }
706 
707     /**
708      * @return mailbox display name, or null if mailbox not found.
709      */
getDisplayName(Context context, long mailboxId)710     public static String getDisplayName(Context context, long mailboxId) {
711         final Uri url = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId);
712         return Utility.getFirstRowString(context, url, MAILBOX_DISPLAY_NAME_PROJECTION,
713                 null, null, null, MAILBOX_DISPLAY_NAME_COLUMN);
714     }
715 
716     /**
717      * @param mailboxId ID of a mailbox.  This method accepts magic mailbox IDs, such as
718      * {@link #QUERY_ALL_INBOXES}. (They're all non-refreshable.)
719      * @return true if a mailbox is refreshable.
720      */
isRefreshable(Context context, long mailboxId)721     public static boolean isRefreshable(Context context, long mailboxId) {
722         if (mailboxId < 0) {
723             return false; // magic mailboxes
724         }
725         switch (getMailboxType(context, mailboxId)) {
726             case -1: // not found
727             case TYPE_DRAFTS:
728             case TYPE_OUTBOX:
729                 return false;
730         }
731         return true;
732     }
733 
734     /**
735      * @return whether or not this mailbox supports moving messages out of it
736      */
canHaveMessagesMoved()737     public boolean canHaveMessagesMoved() {
738         switch (mType) {
739             case TYPE_INBOX:
740             case TYPE_MAIL:
741             case TYPE_TRASH:
742             case TYPE_JUNK:
743                 return true;
744         }
745         return false; // TYPE_DRAFTS, TYPE_OUTBOX, TYPE_SENT, etc
746     }
747 
748     /**
749      * Returns a set of hashes that can identify this mailbox. These can be used to
750      * determine if any of the fields have been modified.
751      */
getHashes()752     public Object[] getHashes() {
753         final Object[] hash = new Object[CONTENT_PROJECTION.length];
754 
755         hash[CONTENT_ID_COLUMN]
756              = mId;
757         hash[CONTENT_DISPLAY_NAME_COLUMN]
758                 = mDisplayName;
759         hash[CONTENT_SERVER_ID_COLUMN]
760                 = mServerId;
761         hash[CONTENT_PARENT_SERVER_ID_COLUMN]
762                 = mParentServerId;
763         hash[CONTENT_ACCOUNT_KEY_COLUMN]
764                 = mAccountKey;
765         hash[CONTENT_TYPE_COLUMN]
766                 = mType;
767         hash[CONTENT_DELIMITER_COLUMN]
768                 = mDelimiter;
769         hash[CONTENT_SYNC_KEY_COLUMN]
770                 = mSyncKey;
771         hash[CONTENT_SYNC_LOOKBACK_COLUMN]
772                 = mSyncLookback;
773         hash[CONTENT_SYNC_INTERVAL_COLUMN]
774                 = mSyncInterval;
775         hash[CONTENT_SYNC_TIME_COLUMN]
776                 = mSyncTime;
777         hash[CONTENT_FLAG_VISIBLE_COLUMN]
778                 = mFlagVisible;
779         hash[CONTENT_FLAGS_COLUMN]
780                 = mFlags;
781         hash[CONTENT_SYNC_STATUS_COLUMN]
782                 = mSyncStatus;
783         hash[CONTENT_PARENT_KEY_COLUMN]
784                 = mParentKey;
785         hash[CONTENT_LAST_TOUCHED_TIME_COLUMN]
786                 = mLastTouchedTime;
787         hash[CONTENT_UI_SYNC_STATUS_COLUMN]
788                 = mUiSyncStatus;
789         hash[CONTENT_UI_LAST_SYNC_RESULT_COLUMN]
790                 = mUiLastSyncResult;
791         hash[CONTENT_TOTAL_COUNT_COLUMN]
792                 = mTotalCount;
793         hash[CONTENT_HIERARCHICAL_NAME_COLUMN]
794                 = mHierarchicalName;
795         return hash;
796     }
797 
798     // Parcelable
799     @Override
describeContents()800     public int describeContents() {
801         return 0;
802     }
803 
804     // Parcelable
805     @Override
writeToParcel(Parcel dest, int flags)806     public void writeToParcel(Parcel dest, int flags) {
807         dest.writeParcelable(mBaseUri, flags);
808         dest.writeLong(mId);
809         dest.writeString(mDisplayName);
810         dest.writeString(mServerId);
811         dest.writeString(mParentServerId);
812         dest.writeLong(mParentKey);
813         dest.writeLong(mAccountKey);
814         dest.writeInt(mType);
815         dest.writeInt(mDelimiter);
816         dest.writeString(mSyncKey);
817         dest.writeInt(mSyncLookback);
818         dest.writeInt(mSyncInterval);
819         dest.writeLong(mSyncTime);
820         dest.writeInt(mFlagVisible ? 1 : 0);
821         dest.writeInt(mFlags);
822         dest.writeString(mSyncStatus);
823         dest.writeLong(mLastTouchedTime);
824         dest.writeInt(mUiSyncStatus);
825         dest.writeInt(mUiLastSyncResult);
826         dest.writeInt(mTotalCount);
827         dest.writeString(mHierarchicalName);
828         dest.writeLong(mLastFullSyncTime);
829     }
830 
Mailbox(Parcel in)831     public Mailbox(Parcel in) {
832         mBaseUri = in.readParcelable(null);
833         mId = in.readLong();
834         mDisplayName = in.readString();
835         mServerId = in.readString();
836         mParentServerId = in.readString();
837         mParentKey = in.readLong();
838         mAccountKey = in.readLong();
839         mType = in.readInt();
840         mDelimiter = in.readInt();
841         mSyncKey = in.readString();
842         mSyncLookback = in.readInt();
843         mSyncInterval = in.readInt();
844         mSyncTime = in.readLong();
845         mFlagVisible = in.readInt() == 1;
846         mFlags = in.readInt();
847         mSyncStatus = in.readString();
848         mLastTouchedTime = in.readLong();
849         mUiSyncStatus = in.readInt();
850         mUiLastSyncResult = in.readInt();
851         mTotalCount = in.readInt();
852         mHierarchicalName = in.readString();
853         mLastFullSyncTime = in.readLong();
854     }
855 
856     public static final Parcelable.Creator<Mailbox> CREATOR = new Parcelable.Creator<Mailbox>() {
857         @Override
858         public Mailbox createFromParcel(Parcel source) {
859             return new Mailbox(source);
860         }
861 
862         @Override
863         public Mailbox[] newArray(int size) {
864             return new Mailbox[size];
865         }
866     };
867 
868     @Override
toString()869     public String toString() {
870         return "[Mailbox " + mId + ": " + mDisplayName + "]";
871     }
872 
873     /**
874      * Get the mailboxes that should receive push updates for an account.
875      * @param cr The {@link ContentResolver}.
876      * @param accountId The id for the account that is pushing.
877      * @return A cursor (suitable for use with {@link #restore}) with all mailboxes we should sync.
878      */
getMailboxesForPush(final ContentResolver cr, final long accountId)879     public static Cursor getMailboxesForPush(final ContentResolver cr, final long accountId) {
880         return cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
881                 PUSH_MAILBOXES_FOR_ACCOUNT_SELECTION, new String[] { Long.toString(accountId) },
882                 null);
883     }
884 
885     /**
886      * Get the mailbox ids for an account that should sync when we do a full account sync.
887      * @param cr The {@link ContentResolver}.
888      * @param accountId The id for the account that is pushing.
889      * @return A cursor (with one column, containing ids) with all mailbox ids we should sync.
890      */
getMailboxIdsForSync(final ContentResolver cr, final long accountId)891     public static Cursor getMailboxIdsForSync(final ContentResolver cr, final long accountId) {
892         // We're sorting by mailbox type. The reason is that the inbox is type 0, other types
893         // (e.g. Calendar and Contacts) are all higher numbers. Upon initial sync, we'd like to
894         // sync the inbox first to improve perceived performance.
895         return cr.query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION,
896                 OUTBOX_PLUS_SYNCING_AND_ACCOUNT_SELECTION,
897                 new String[] { Long.toString(accountId) }, MailboxColumns.TYPE + " ASC");
898     }
899 
900     /**
901      * Get the mailbox ids for an account that are configured for sync and have a specific type.
902      * @param accountId The id for the account that is syncing.
903      * @param mailboxType The type of the mailbox we're interested in.
904      * @return A cursor (with one column, containing ids) with all mailbox ids that match.
905      */
getMailboxIdsForSyncByType(final ContentResolver cr, final long accountId, final int mailboxType)906     public static Cursor getMailboxIdsForSyncByType(final ContentResolver cr, final long accountId,
907             final int mailboxType) {
908         return cr.query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION,
909                 SYNCING_AND_TYPE_FOR_ACCOUNT_SELECTION,
910                 new String[] { Integer.toString(mailboxType), Long.toString(accountId) }, null);
911     }
912 
913     /**
914      * Get the account id for a mailbox.
915      * @param context The {@link Context}.
916      * @param mailboxId The id of the mailbox we're interested in, as a {@link String}.
917      * @return The account id for the mailbox, or {@link Account#NO_ACCOUNT} if the mailbox doesn't
918      *         exist.
919      */
getAccountIdForMailbox(final Context context, final String mailboxId)920     public static long getAccountIdForMailbox(final Context context, final String mailboxId) {
921         return Utility.getFirstRowLong(context,
922                 Mailbox.CONTENT_URI.buildUpon().appendEncodedPath(mailboxId).build(),
923                 ACCOUNT_KEY_PROJECTION, null, null, null,
924                 ACCOUNT_KEY_PROJECTION_ACCOUNT_KEY_COLUMN, Account.NO_ACCOUNT);
925     }
926 
927     /**
928      * Gets the correct authority for a mailbox.
929      * @param mailboxType The type of the mailbox we're interested in.
930      * @return The authority for the mailbox we're interested in.
931      */
getAuthority(final int mailboxType)932     public static String getAuthority(final int mailboxType) {
933         switch (mailboxType) {
934             case Mailbox.TYPE_CALENDAR:
935                 return CalendarContract.AUTHORITY;
936             case Mailbox.TYPE_CONTACTS:
937                 return ContactsContract.AUTHORITY;
938             default:
939                 return EmailContent.AUTHORITY;
940         }
941     }
942 
resyncMailbox(final ContentResolver cr, final android.accounts.Account account, final long mailboxId)943     public static void resyncMailbox(final ContentResolver cr,
944             final android.accounts.Account account, final long mailboxId) {
945         final Cursor cursor = cr.query(Mailbox.CONTENT_URI,
946                 new String[]{
947                         MailboxColumns.TYPE,
948                         MailboxColumns.SERVER_ID,
949                 },
950                 MailboxColumns._ID + "=?",
951                 new String[] {String.valueOf(mailboxId)},
952                 null);
953         if (cursor == null || cursor.getCount() == 0) {
954             LogUtils.w(Logging.LOG_TAG, "Mailbox %d not found", mailboxId);
955             return;
956         }
957         try {
958             cursor.moveToFirst();
959             final int type = cursor.getInt(0);
960             if (type >= TYPE_NOT_EMAIL) {
961                 throw new IllegalArgumentException(
962                         String.format("Mailbox %d is not an Email mailbox", mailboxId));
963             }
964             final String serverId = cursor.getString(1);
965             if (TextUtils.isEmpty(serverId)) {
966                 throw new IllegalArgumentException(
967                         String.format("Mailbox %d has no server id", mailboxId));
968             }
969             final ArrayList<ContentProviderOperation> ops =
970                     new ArrayList<ContentProviderOperation>();
971             ops.add(ContentProviderOperation.newDelete(Message.CONTENT_URI)
972                     .withSelection(Message.MAILBOX_SELECTION,
973                             new String[]{String.valueOf(mailboxId)})
974                     .build());
975             ops.add(ContentProviderOperation.newUpdate(
976                     ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId))
977                     .withValue(MailboxColumns.SYNC_KEY, "0").build());
978 
979             cr.applyBatch(AUTHORITY, ops);
980             final Bundle extras = createSyncBundle(mailboxId);
981             extras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true);
982             ContentResolver.requestSync(account, AUTHORITY, extras);
983             LogUtils.i(Logging.LOG_TAG, "requestSync resyncMailbox %s, %s",
984                     account.toString(), extras.toString());
985         } catch (RemoteException e) {
986             LogUtils.w(Logging.LOG_TAG, e, "Failed to wipe mailbox %d", mailboxId);
987         } catch (OperationApplicationException e) {
988             LogUtils.w(Logging.LOG_TAG, e, "Failed to wipe mailbox %d", mailboxId);
989         } finally {
990             cursor.close();
991         }
992     }
993 }
994