1/*
2 * Copyright (C) 2011 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
17package com.android.emailcommon.provider;
18
19import android.content.ContentProviderOperation;
20import android.content.ContentProviderResult;
21import android.content.ContentResolver;
22import android.content.ContentUris;
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.OperationApplicationException;
26import android.database.Cursor;
27import android.net.ConnectivityManager;
28import android.net.NetworkInfo;
29import android.net.Uri;
30import android.os.Parcel;
31import android.os.Parcelable;
32import android.os.RemoteException;
33
34import com.android.emailcommon.provider.EmailContent.AccountColumns;
35import com.android.emailcommon.utility.Utility;
36
37import java.util.ArrayList;
38import java.util.List;
39import java.util.UUID;
40
41public final class Account extends EmailContent implements AccountColumns, Parcelable {
42    public static final String TABLE_NAME = "Account";
43    @SuppressWarnings("hiding")
44    public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/account");
45    public static final Uri ADD_TO_FIELD_URI =
46        Uri.parse(EmailContent.CONTENT_URI + "/accountIdAddToField");
47    public static final Uri RESET_NEW_MESSAGE_COUNT_URI =
48        Uri.parse(EmailContent.CONTENT_URI + "/resetNewMessageCount");
49    public static final Uri NOTIFIER_URI =
50        Uri.parse(EmailContent.CONTENT_NOTIFIER_URI + "/account");
51    public static final Uri DEFAULT_ACCOUNT_ID_URI =
52        Uri.parse(EmailContent.CONTENT_URI + "/account/default");
53
54    // Define all pseudo account IDs here to avoid conflict with one another.
55    /**
56     * Pseudo account ID to represent a "combined account" that includes messages and mailboxes
57     * from all defined accounts.
58     *
59     * <em>IMPORTANT</em>: This must never be stored to the database.
60     */
61    public static final long ACCOUNT_ID_COMBINED_VIEW = 0x1000000000000000L;
62    /**
63     * Pseudo account ID to represent "no account". This may be used any time the account ID
64     * may not be known or when we want to specifically select "no" account.
65     *
66     * <em>IMPORTANT</em>: This must never be stored to the database.
67     */
68    public static final long NO_ACCOUNT = -1L;
69
70    // Whether or not the user has asked for notifications of new mail in this account
71    public final static int FLAGS_NOTIFY_NEW_MAIL = 1<<0;
72    // Whether or not the user has asked for vibration notifications with all new mail
73    public final static int FLAGS_VIBRATE_ALWAYS = 1<<1;
74    // Bit mask for the account's deletion policy (see DELETE_POLICY_x below)
75    public static final int FLAGS_DELETE_POLICY_MASK = 1<<2 | 1<<3;
76    public static final int FLAGS_DELETE_POLICY_SHIFT = 2;
77    // Whether the account is in the process of being created; any account reconciliation code
78    // MUST ignore accounts with this bit set; in addition, ContentObservers for this data
79    // SHOULD consider the state of this flag during operation
80    public static final int FLAGS_INCOMPLETE = 1<<4;
81    // Security hold is used when the device is not in compliance with security policies
82    // required by the server; in this state, the user MUST be alerted to the need to update
83    // security settings.  Sync adapters SHOULD NOT attempt to sync when this flag is set.
84    public static final int FLAGS_SECURITY_HOLD = 1<<5;
85    // Whether or not the user has asked for vibration notifications when the ringer is silent
86    public static final int FLAGS_VIBRATE_WHEN_SILENT = 1<<6;
87    // Whether the account supports "smart forward" (i.e. the server appends the original
88    // message along with any attachments to the outgoing message)
89    public static final int FLAGS_SUPPORTS_SMART_FORWARD = 1<<7;
90    // Whether the account should try to cache attachments in the background
91    public static final int FLAGS_BACKGROUND_ATTACHMENTS = 1<<8;
92    // Available to sync adapter
93    public static final int FLAGS_SYNC_ADAPTER = 1<<9;
94    // Sync disabled is a status commanded by the server; the sync adapter SHOULD NOT try to
95    // sync mailboxes in this account automatically.  A manual sync request to sync a mailbox
96    // with sync disabled SHOULD try to sync and report any failure result via the UI.
97    public static final int FLAGS_SYNC_DISABLED = 1<<10;
98    // Whether or not server-side search is supported by this account
99    public static final int FLAGS_SUPPORTS_SEARCH = 1<<11;
100    // Whether or not server-side search supports global search (i.e. all mailboxes); only valid
101    // if FLAGS_SUPPORTS_SEARCH is true
102    public static final int FLAGS_SUPPORTS_GLOBAL_SEARCH = 1<<12;
103
104    // Deletion policy (see FLAGS_DELETE_POLICY_MASK, above)
105    public static final int DELETE_POLICY_NEVER = 0;
106    public static final int DELETE_POLICY_7DAYS = 1<<0;        // not supported
107    public static final int DELETE_POLICY_ON_DELETE = 1<<1;
108
109    // Sentinel values for the mSyncInterval field of both Account records
110    public static final int CHECK_INTERVAL_NEVER = -1;
111    public static final int CHECK_INTERVAL_PUSH = -2;
112
113    public String mDisplayName;
114    public String mEmailAddress;
115    public String mSyncKey;
116    public int mSyncLookback;
117    public int mSyncInterval;
118    public long mHostAuthKeyRecv;
119    public long mHostAuthKeySend;
120    public int mFlags;
121    public boolean mIsDefault;          // note: callers should use getDefaultAccountId()
122    public String mCompatibilityUuid;
123    public String mSenderName;
124    public String mRingtoneUri;
125    public String mProtocolVersion;
126    public int mNewMessageCount;
127    public String mSecuritySyncKey;
128    public String mSignature;
129    public long mPolicyKey;
130
131    // For compatibility with Email1
132    public long mNotifiedMessageId;
133    public int mNotifiedMessageCount;
134
135    // Convenience for creating/working with an account
136    public transient HostAuth mHostAuthRecv;
137    public transient HostAuth mHostAuthSend;
138    public transient Policy mPolicy;
139    // Might hold the corresponding AccountManager account structure
140    public transient android.accounts.Account mAmAccount;
141
142    public static final int CONTENT_ID_COLUMN = 0;
143    public static final int CONTENT_DISPLAY_NAME_COLUMN = 1;
144    public static final int CONTENT_EMAIL_ADDRESS_COLUMN = 2;
145    public static final int CONTENT_SYNC_KEY_COLUMN = 3;
146    public static final int CONTENT_SYNC_LOOKBACK_COLUMN = 4;
147    public static final int CONTENT_SYNC_INTERVAL_COLUMN = 5;
148    public static final int CONTENT_HOST_AUTH_KEY_RECV_COLUMN = 6;
149    public static final int CONTENT_HOST_AUTH_KEY_SEND_COLUMN = 7;
150    public static final int CONTENT_FLAGS_COLUMN = 8;
151    public static final int CONTENT_IS_DEFAULT_COLUMN = 9;
152    public static final int CONTENT_COMPATIBILITY_UUID_COLUMN = 10;
153    public static final int CONTENT_SENDER_NAME_COLUMN = 11;
154    public static final int CONTENT_RINGTONE_URI_COLUMN = 12;
155    public static final int CONTENT_PROTOCOL_VERSION_COLUMN = 13;
156    public static final int CONTENT_NEW_MESSAGE_COUNT_COLUMN = 14;
157    public static final int CONTENT_SECURITY_SYNC_KEY_COLUMN = 15;
158    public static final int CONTENT_SIGNATURE_COLUMN = 16;
159    public static final int CONTENT_POLICY_KEY = 17;
160    public static final int CONTENT_NOTIFIED_MESSAGE_ID_COLUMN = 18;
161    public static final int CONTENT_NOTIFIED_MESSAGE_COUNT_COLUMN = 19;
162
163    public static final String[] CONTENT_PROJECTION = new String[] {
164        RECORD_ID, AccountColumns.DISPLAY_NAME,
165        AccountColumns.EMAIL_ADDRESS, AccountColumns.SYNC_KEY, AccountColumns.SYNC_LOOKBACK,
166        AccountColumns.SYNC_INTERVAL, AccountColumns.HOST_AUTH_KEY_RECV,
167        AccountColumns.HOST_AUTH_KEY_SEND, AccountColumns.FLAGS, AccountColumns.IS_DEFAULT,
168        AccountColumns.COMPATIBILITY_UUID, AccountColumns.SENDER_NAME,
169        AccountColumns.RINGTONE_URI, AccountColumns.PROTOCOL_VERSION,
170        AccountColumns.NEW_MESSAGE_COUNT, AccountColumns.SECURITY_SYNC_KEY,
171        AccountColumns.SIGNATURE, AccountColumns.POLICY_KEY,
172        AccountColumns.NOTIFIED_MESSAGE_ID, AccountColumns.NOTIFIED_MESSAGE_COUNT
173    };
174
175    public static final int CONTENT_MAILBOX_TYPE_COLUMN = 1;
176
177    /**
178     * This projection is for listing account id's only
179     */
180    public static final String[] ID_TYPE_PROJECTION = new String[] {
181        RECORD_ID, MailboxColumns.TYPE
182    };
183
184    public static final int ACCOUNT_FLAGS_COLUMN_ID = 0;
185    public static final int ACCOUNT_FLAGS_COLUMN_FLAGS = 1;
186    public static final String[] ACCOUNT_FLAGS_PROJECTION = new String[] {
187            AccountColumns.ID, AccountColumns.FLAGS};
188
189    public static final String MAILBOX_SELECTION =
190        MessageColumns.MAILBOX_KEY + " =?";
191
192    public static final String UNREAD_COUNT_SELECTION =
193        MessageColumns.MAILBOX_KEY + " =? and " + MessageColumns.FLAG_READ + "= 0";
194
195    private static final String UUID_SELECTION = AccountColumns.COMPATIBILITY_UUID + " =?";
196
197    public static final String SECURITY_NONZERO_SELECTION =
198        Account.POLICY_KEY + " IS NOT NULL AND " + Account.POLICY_KEY + "!=0";
199
200    private static final String FIND_INBOX_SELECTION =
201            MailboxColumns.TYPE + " = " + Mailbox.TYPE_INBOX +
202            " AND " + MailboxColumns.ACCOUNT_KEY + " =?";
203
204    /**
205     * This projection is for searching for the default account
206     */
207    private static final String[] DEFAULT_ID_PROJECTION = new String[] {
208        RECORD_ID, IS_DEFAULT
209    };
210
211    /**
212     * no public constructor since this is a utility class
213     */
214    public Account() {
215        mBaseUri = CONTENT_URI;
216
217        // other defaults (policy)
218        mRingtoneUri = "content://settings/system/notification_sound";
219        mSyncInterval = -1;
220        mSyncLookback = -1;
221        mFlags = FLAGS_NOTIFY_NEW_MAIL;
222        mCompatibilityUuid = UUID.randomUUID().toString();
223    }
224
225    public static Account restoreAccountWithId(Context context, long id) {
226        return EmailContent.restoreContentWithId(context, Account.class,
227                Account.CONTENT_URI, Account.CONTENT_PROJECTION, id);
228    }
229
230    /**
231     * Returns {@code true} if the given account ID is a "normal" account. Normal accounts
232     * always have an ID greater than {@code 0} and not equal to any pseudo account IDs
233     * (such as {@link #ACCOUNT_ID_COMBINED_VIEW})
234     */
235    public static boolean isNormalAccount(long accountId) {
236        return (accountId > 0L) && (accountId != ACCOUNT_ID_COMBINED_VIEW);
237    }
238
239    /**
240     * Refresh an account that has already been loaded.  This is slightly less expensive
241     * that generating a brand-new account object.
242     */
243    public void refresh(Context context) {
244        Cursor c = context.getContentResolver().query(getUri(), Account.CONTENT_PROJECTION,
245                null, null, null);
246        try {
247            c.moveToFirst();
248            restore(c);
249        } finally {
250            if (c != null) {
251                c.close();
252            }
253        }
254    }
255
256    @Override
257    public void restore(Cursor cursor) {
258        mId = cursor.getLong(CONTENT_ID_COLUMN);
259        mBaseUri = CONTENT_URI;
260        mDisplayName = cursor.getString(CONTENT_DISPLAY_NAME_COLUMN);
261        mEmailAddress = cursor.getString(CONTENT_EMAIL_ADDRESS_COLUMN);
262        mSyncKey = cursor.getString(CONTENT_SYNC_KEY_COLUMN);
263        mSyncLookback = cursor.getInt(CONTENT_SYNC_LOOKBACK_COLUMN);
264        mSyncInterval = cursor.getInt(CONTENT_SYNC_INTERVAL_COLUMN);
265        mHostAuthKeyRecv = cursor.getLong(CONTENT_HOST_AUTH_KEY_RECV_COLUMN);
266        mHostAuthKeySend = cursor.getLong(CONTENT_HOST_AUTH_KEY_SEND_COLUMN);
267        mFlags = cursor.getInt(CONTENT_FLAGS_COLUMN);
268        mIsDefault = cursor.getInt(CONTENT_IS_DEFAULT_COLUMN) == 1;
269        mCompatibilityUuid = cursor.getString(CONTENT_COMPATIBILITY_UUID_COLUMN);
270        mSenderName = cursor.getString(CONTENT_SENDER_NAME_COLUMN);
271        mRingtoneUri = cursor.getString(CONTENT_RINGTONE_URI_COLUMN);
272        mProtocolVersion = cursor.getString(CONTENT_PROTOCOL_VERSION_COLUMN);
273        mNewMessageCount = cursor.getInt(CONTENT_NEW_MESSAGE_COUNT_COLUMN);
274        mSecuritySyncKey = cursor.getString(CONTENT_SECURITY_SYNC_KEY_COLUMN);
275        mSignature = cursor.getString(CONTENT_SIGNATURE_COLUMN);
276        mPolicyKey = cursor.getLong(CONTENT_POLICY_KEY);
277        mNotifiedMessageId = cursor.getLong(CONTENT_NOTIFIED_MESSAGE_ID_COLUMN);
278        mNotifiedMessageCount = cursor.getInt(CONTENT_NOTIFIED_MESSAGE_COUNT_COLUMN);
279    }
280
281    private long getId(Uri u) {
282        return Long.parseLong(u.getPathSegments().get(1));
283    }
284
285    /**
286     * @return the user-visible name for the account
287     */
288    public String getDisplayName() {
289        return mDisplayName;
290    }
291
292    /**
293     * Set the description.  Be sure to call save() to commit to database.
294     * @param description the new description
295     */
296    public void setDisplayName(String description) {
297        mDisplayName = description;
298    }
299
300    /**
301     * @return the email address for this account
302     */
303    public String getEmailAddress() {
304        return mEmailAddress;
305    }
306
307    /**
308     * Set the Email address for this account.  Be sure to call save() to commit to database.
309     * @param emailAddress the new email address for this account
310     */
311    public void setEmailAddress(String emailAddress) {
312        mEmailAddress = emailAddress;
313    }
314
315    /**
316     * @return the sender's name for this account
317     */
318    public String getSenderName() {
319        return mSenderName;
320    }
321
322    /**
323     * Set the sender's name.  Be sure to call save() to commit to database.
324     * @param name the new sender name
325     */
326    public void setSenderName(String name) {
327        mSenderName = name;
328    }
329
330    public String getSignature() {
331        return mSignature;
332    }
333
334    public void setSignature(String signature) {
335        mSignature = signature;
336    }
337
338    /**
339     * @return the minutes per check (for polling)
340     * TODO define sentinel values for "never", "push", etc.  See Account.java
341     */
342    public int getSyncInterval() {
343        return mSyncInterval;
344    }
345
346    /**
347     * Set the minutes per check (for polling).  Be sure to call save() to commit to database.
348     * TODO define sentinel values for "never", "push", etc.  See Account.java
349     * @param minutes the number of minutes between polling checks
350     */
351    public void setSyncInterval(int minutes) {
352        mSyncInterval = minutes;
353    }
354
355    /**
356     * @return One of the {@code Account.SYNC_WINDOW_*} constants that represents the sync
357     *     lookback window.
358     * TODO define sentinel values for "all", "1 month", etc.  See Account.java
359     */
360    public int getSyncLookback() {
361        return mSyncLookback;
362    }
363
364    /**
365     * Set the sync lookback window.  Be sure to call save() to commit to database.
366     * TODO define sentinel values for "all", "1 month", etc.  See Account.java
367     * @param value One of the {@link com.android.emailcommon.service.SyncWindow} constants
368     */
369    public void setSyncLookback(int value) {
370        mSyncLookback = value;
371    }
372
373    /**
374     * @return the flags for this account
375     * @see #FLAGS_NOTIFY_NEW_MAIL
376     * @see #FLAGS_VIBRATE_ALWAYS
377     * @see #FLAGS_VIBRATE_WHEN_SILENT
378     */
379    public int getFlags() {
380        return mFlags;
381    }
382
383    /**
384     * Set the flags for this account
385     * @see #FLAGS_NOTIFY_NEW_MAIL
386     * @see #FLAGS_VIBRATE_ALWAYS
387     * @see #FLAGS_VIBRATE_WHEN_SILENT
388     * @param newFlags the new value for the flags
389     */
390    public void setFlags(int newFlags) {
391        mFlags = newFlags;
392    }
393
394    /**
395     * @return the ringtone Uri for this account
396     */
397    public String getRingtone() {
398        return mRingtoneUri;
399    }
400
401    /**
402     * Set the ringtone Uri for this account
403     * @param newUri the new URI string for the ringtone for this account
404     */
405    public void setRingtone(String newUri) {
406        mRingtoneUri = newUri;
407    }
408
409    /**
410     * Set the "delete policy" as a simple 0,1,2 value set.
411     * @param newPolicy the new delete policy
412     */
413    public void setDeletePolicy(int newPolicy) {
414        mFlags &= ~FLAGS_DELETE_POLICY_MASK;
415        mFlags |= (newPolicy << FLAGS_DELETE_POLICY_SHIFT) & FLAGS_DELETE_POLICY_MASK;
416    }
417
418    /**
419     * Return the "delete policy" as a simple 0,1,2 value set.
420     * @return the current delete policy
421     */
422    public int getDeletePolicy() {
423        return (mFlags & FLAGS_DELETE_POLICY_MASK) >> FLAGS_DELETE_POLICY_SHIFT;
424    }
425
426    /**
427     * Return the Uuid associated with this account.  This is primarily for compatibility
428     * with accounts set up by previous versions, because there are externals references
429     * to the Uuid (e.g. desktop shortcuts).
430     */
431    public String getUuid() {
432        return mCompatibilityUuid;
433    }
434
435    public HostAuth getOrCreateHostAuthSend(Context context) {
436        if (mHostAuthSend == null) {
437            if (mHostAuthKeySend != 0) {
438                mHostAuthSend = HostAuth.restoreHostAuthWithId(context, mHostAuthKeySend);
439            } else {
440                mHostAuthSend = new HostAuth();
441            }
442        }
443        return mHostAuthSend;
444    }
445
446    public HostAuth getOrCreateHostAuthRecv(Context context) {
447        if (mHostAuthRecv == null) {
448            if (mHostAuthKeyRecv != 0) {
449                mHostAuthRecv = HostAuth.restoreHostAuthWithId(context, mHostAuthKeyRecv);
450            } else {
451                mHostAuthRecv = new HostAuth();
452            }
453        }
454        return mHostAuthRecv;
455    }
456
457    /**
458     * For compatibility while converting to provider model, generate a "local store URI"
459     *
460     * @return a string in the form of a Uri, as used by the other parts of the email app
461     */
462    public String getLocalStoreUri(Context context) {
463        return "local://localhost/" + context.getDatabasePath(getUuid() + ".db");
464    }
465
466    /**
467     * @return true if the instance is of an EAS account.
468     *
469     * NOTE This method accesses the DB if {@link #mHostAuthRecv} hasn't been restored yet.
470     * Use caution when you use this on the main thread.
471     */
472    public boolean isEasAccount(Context context) {
473        return "eas".equals(getProtocol(context));
474    }
475
476    public boolean supportsMoveMessages(Context context) {
477        String protocol = getProtocol(context);
478        return "eas".equals(protocol) || "imap".equals(protocol);
479    }
480
481    /**
482     * @return true if the account supports "search".
483     */
484    public static boolean supportsServerSearch(Context context, long accountId) {
485        Account account = Account.restoreAccountWithId(context, accountId);
486        if (account == null) return false;
487        return (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) != 0;
488    }
489
490    /**
491     * Set the account to be the default account.  If this is set to "true", when the account
492     * is saved, all other accounts will have the same value set to "false".
493     * @param newDefaultState the new default state - if true, others will be cleared.
494     */
495    public void setDefaultAccount(boolean newDefaultState) {
496        mIsDefault = newDefaultState;
497    }
498
499    /**
500     * @return {@link Uri} to this {@link Account} in the
501     * {@code content://com.android.email.provider/account/UUID} format, which is safe to use
502     * for desktop shortcuts.
503     *
504     * <p>We don't want to store _id in shortcuts, because
505     * {@link com.android.email.provider.AccountBackupRestore} won't preserve it.
506     */
507    public Uri getShortcutSafeUri() {
508        return getShortcutSafeUriFromUuid(mCompatibilityUuid);
509    }
510
511    /**
512     * @return {@link Uri} to an {@link Account} with a {@code uuid}.
513     */
514    public static Uri getShortcutSafeUriFromUuid(String uuid) {
515        return CONTENT_URI.buildUpon().appendEncodedPath(uuid).build();
516    }
517
518    /**
519     * Parse {@link Uri} in the {@code content://com.android.email.provider/account/ID} format
520     * where ID = account id (used on Eclair, Android 2.0-2.1) or UUID, and return _id of
521     * the {@link Account} associated with it.
522     *
523     * @param context context to access DB
524     * @param uri URI of interest
525     * @return _id of the {@link Account} associated with ID, or -1 if none found.
526     */
527    public static long getAccountIdFromShortcutSafeUri(Context context, Uri uri) {
528        // Make sure the URI is in the correct format.
529        if (!"content".equals(uri.getScheme())
530                || !AUTHORITY.equals(uri.getAuthority())) {
531            return -1;
532        }
533
534        final List<String> ps = uri.getPathSegments();
535        if (ps.size() != 2 || !"account".equals(ps.get(0))) {
536            return -1;
537        }
538
539        // Now get the ID part.
540        final String id = ps.get(1);
541
542        // First, see if ID can be parsed as long.  (Eclair-style)
543        // (UUIDs have '-' in them, so they are always non-parsable.)
544        try {
545            return Long.parseLong(id);
546        } catch (NumberFormatException ok) {
547            // OK, it's not a long.  Continue...
548        }
549
550        // Now id is a UUId.
551        return getAccountIdFromUuid(context, id);
552    }
553
554    /**
555     * @return ID of the account with the given UUID.
556     */
557    public static long getAccountIdFromUuid(Context context, String uuid) {
558        return Utility.getFirstRowLong(context,
559                CONTENT_URI, ID_PROJECTION,
560                UUID_SELECTION, new String[] {uuid}, null, 0, -1L);
561    }
562
563    /**
564     * Return the id of the default account.  If one hasn't been explicitly specified, return
565     * the first one in the database (the logic is provided within EmailProvider)
566     * @param context the caller's context
567     * @return the id of the default account, or Account.NO_ACCOUNT if there are no accounts
568     */
569    static public long getDefaultAccountId(Context context) {
570        Cursor c = context.getContentResolver().query(
571                Account.DEFAULT_ACCOUNT_ID_URI, Account.ID_PROJECTION, null, null, null);
572        try {
573            if (c != null && c.moveToFirst()) {
574                return c.getLong(Account.ID_PROJECTION_COLUMN);
575            }
576        } finally {
577            c.close();
578        }
579        return Account.NO_ACCOUNT;
580    }
581
582    /**
583     * Given an account id, return the account's protocol
584     * @param context the caller's context
585     * @param accountId the id of the account to be examined
586     * @return the account's protocol (or null if the Account or HostAuth do not exist)
587     */
588    public static String getProtocol(Context context, long accountId) {
589        Account account = Account.restoreAccountWithId(context, accountId);
590        if (account != null) {
591            return account.getProtocol(context);
592         }
593        return null;
594    }
595
596    /**
597     * Return the account's protocol
598     * @param context the caller's context
599     * @return the account's protocol (or null if the HostAuth doesn't not exist)
600     */
601    public String getProtocol(Context context) {
602        HostAuth hostAuth = HostAuth.restoreHostAuthWithId(context, mHostAuthKeyRecv);
603        if (hostAuth != null) {
604            return hostAuth.mProtocol;
605        }
606        return null;
607    }
608
609    /**
610     * Return the account ID for a message with a given id
611     *
612     * @param context the caller's context
613     * @param messageId the id of the message
614     * @return the account ID, or -1 if the account doesn't exist
615     */
616    public static long getAccountIdForMessageId(Context context, long messageId) {
617        return Message.getKeyColumnLong(context, messageId, MessageColumns.ACCOUNT_KEY);
618    }
619
620    /**
621     * Return the account for a message with a given id
622     * @param context the caller's context
623     * @param messageId the id of the message
624     * @return the account, or null if the account doesn't exist
625     */
626    public static Account getAccountForMessageId(Context context, long messageId) {
627        long accountId = getAccountIdForMessageId(context, messageId);
628        if (accountId != -1) {
629            return Account.restoreAccountWithId(context, accountId);
630        }
631        return null;
632    }
633
634    /**
635     * @return true if an {@code accountId} is assigned to any existing account.
636     */
637    public static boolean isValidId(Context context, long accountId) {
638        return null != Utility.getFirstRowLong(context, CONTENT_URI, ID_PROJECTION,
639                ID_SELECTION, new String[] {Long.toString(accountId)}, null,
640                ID_PROJECTION_COLUMN);
641    }
642
643    /**
644     * Check a single account for security hold status.
645     */
646    public static boolean isSecurityHold(Context context, long accountId) {
647        return (Utility.getFirstRowLong(context,
648                ContentUris.withAppendedId(Account.CONTENT_URI, accountId),
649                ACCOUNT_FLAGS_PROJECTION, null, null, null, ACCOUNT_FLAGS_COLUMN_FLAGS, 0L)
650                & Account.FLAGS_SECURITY_HOLD) != 0;
651    }
652
653    /**
654     * @return id of the "inbox" mailbox, or -1 if not found.
655     */
656    public static long getInboxId(Context context, long accountId) {
657        return Utility.getFirstRowLong(context, Mailbox.CONTENT_URI, ID_PROJECTION,
658                FIND_INBOX_SELECTION, new String[] {Long.toString(accountId)}, null,
659                ID_PROJECTION_COLUMN, -1L);
660    }
661
662    /**
663     * Clear all account hold flags that are set.
664     *
665     * (This will trigger watchers, and in particular will cause EAS to try and resync the
666     * account(s).)
667     */
668    public static void clearSecurityHoldOnAllAccounts(Context context) {
669        ContentResolver resolver = context.getContentResolver();
670        Cursor c = resolver.query(Account.CONTENT_URI, ACCOUNT_FLAGS_PROJECTION,
671                SECURITY_NONZERO_SELECTION, null, null);
672        try {
673            while (c.moveToNext()) {
674                int flags = c.getInt(ACCOUNT_FLAGS_COLUMN_FLAGS);
675
676                if (0 != (flags & FLAGS_SECURITY_HOLD)) {
677                    ContentValues cv = new ContentValues();
678                    cv.put(AccountColumns.FLAGS, flags & ~FLAGS_SECURITY_HOLD);
679                    long accountId = c.getLong(ACCOUNT_FLAGS_COLUMN_ID);
680                    Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
681                    resolver.update(uri, cv, null, null);
682                }
683            }
684        } finally {
685            c.close();
686        }
687    }
688
689    /**
690     * Given an account id, determine whether the account is currently prohibited from automatic
691     * sync, due to roaming while the account's policy disables this
692     * @param context the caller's context
693     * @param accountId the account id
694     * @return true if the account can't automatically sync due to roaming; false otherwise
695     */
696    public static boolean isAutomaticSyncDisabledByRoaming(Context context, long accountId) {
697        Account account = Account.restoreAccountWithId(context, accountId);
698        // Account being deleted; just return
699        if (account == null) return false;
700        long policyKey = account.mPolicyKey;
701        // If no security policy, we're good
702        if (policyKey <= 0) return false;
703
704        ConnectivityManager cm =
705            (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
706        NetworkInfo info = cm.getActiveNetworkInfo();
707        // If we're not on mobile, we're good
708        if (info == null || (info.getType() != ConnectivityManager.TYPE_MOBILE)) return false;
709        // If we're not roaming, we're good
710        if (!info.isRoaming()) return false;
711        Policy policy = Policy.restorePolicyWithId(context, policyKey);
712        // Account being deleted; just return
713        if (policy == null) return false;
714        return policy.mRequireManualSyncWhenRoaming;
715    }
716
717    /**
718     * Override update to enforce a single default account, and do it atomically
719     */
720    @Override
721    public int update(Context context, ContentValues cv) {
722        if (cv.containsKey(AccountColumns.IS_DEFAULT) &&
723                cv.getAsBoolean(AccountColumns.IS_DEFAULT)) {
724            ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
725            ContentValues cv1 = new ContentValues();
726            cv1.put(AccountColumns.IS_DEFAULT, false);
727            // Clear the default flag in all accounts
728            ops.add(ContentProviderOperation.newUpdate(CONTENT_URI).withValues(cv1).build());
729            // Update this account
730            ops.add(ContentProviderOperation
731                    .newUpdate(ContentUris.withAppendedId(CONTENT_URI, mId))
732                    .withValues(cv).build());
733            try {
734                context.getContentResolver().applyBatch(AUTHORITY, ops);
735                return 1;
736            } catch (RemoteException e) {
737                // There is nothing to be done here; fail by returning 0
738            } catch (OperationApplicationException e) {
739                // There is nothing to be done here; fail by returning 0
740            }
741            return 0;
742        }
743        return super.update(context, cv);
744    }
745
746    /*
747     * Override this so that we can store the HostAuth's first and link them to the Account
748     * (non-Javadoc)
749     * @see com.android.email.provider.EmailContent#save(android.content.Context)
750     */
751    @Override
752    public Uri save(Context context) {
753        if (isSaved()) {
754            throw new UnsupportedOperationException();
755        }
756        // This logic is in place so I can (a) short circuit the expensive stuff when
757        // possible, and (b) override (and throw) if anyone tries to call save() or update()
758        // directly for Account, which are unsupported.
759        if (mHostAuthRecv == null && mHostAuthSend == null && mIsDefault == false &&
760                mPolicy != null) {
761            return super.save(context);
762        }
763
764        int index = 0;
765        int recvIndex = -1;
766        int sendIndex = -1;
767
768        // Create operations for saving the send and recv hostAuths
769        // Also, remember which operation in the array they represent
770        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
771        if (mHostAuthRecv != null) {
772            recvIndex = index++;
773            ops.add(ContentProviderOperation.newInsert(mHostAuthRecv.mBaseUri)
774                    .withValues(mHostAuthRecv.toContentValues())
775                    .build());
776        }
777        if (mHostAuthSend != null) {
778            sendIndex = index++;
779            ops.add(ContentProviderOperation.newInsert(mHostAuthSend.mBaseUri)
780                    .withValues(mHostAuthSend.toContentValues())
781                    .build());
782        }
783
784        // Create operations for making this the only default account
785        // Note, these are always updates because they change existing accounts
786        if (mIsDefault) {
787            index++;
788            ContentValues cv1 = new ContentValues();
789            cv1.put(AccountColumns.IS_DEFAULT, 0);
790            ops.add(ContentProviderOperation.newUpdate(CONTENT_URI).withValues(cv1).build());
791        }
792
793        // Now do the Account
794        ContentValues cv = null;
795        if (recvIndex >= 0 || sendIndex >= 0) {
796            cv = new ContentValues();
797            if (recvIndex >= 0) {
798                cv.put(Account.HOST_AUTH_KEY_RECV, recvIndex);
799            }
800            if (sendIndex >= 0) {
801                cv.put(Account.HOST_AUTH_KEY_SEND, sendIndex);
802            }
803        }
804
805        ContentProviderOperation.Builder b = ContentProviderOperation.newInsert(mBaseUri);
806        b.withValues(toContentValues());
807        if (cv != null) {
808            b.withValueBackReferences(cv);
809        }
810        ops.add(b.build());
811
812        try {
813            ContentProviderResult[] results =
814                context.getContentResolver().applyBatch(AUTHORITY, ops);
815            // If saving, set the mId's of the various saved objects
816            if (recvIndex >= 0) {
817                long newId = getId(results[recvIndex].uri);
818                mHostAuthKeyRecv = newId;
819                mHostAuthRecv.mId = newId;
820            }
821            if (sendIndex >= 0) {
822                long newId = getId(results[sendIndex].uri);
823                mHostAuthKeySend = newId;
824                mHostAuthSend.mId = newId;
825            }
826            Uri u = results[index].uri;
827            mId = getId(u);
828            return u;
829        } catch (RemoteException e) {
830            // There is nothing to be done here; fail by returning null
831        } catch (OperationApplicationException e) {
832            // There is nothing to be done here; fail by returning null
833        }
834        return null;
835    }
836
837    @Override
838    public ContentValues toContentValues() {
839        ContentValues values = new ContentValues();
840        values.put(AccountColumns.DISPLAY_NAME, mDisplayName);
841        values.put(AccountColumns.EMAIL_ADDRESS, mEmailAddress);
842        values.put(AccountColumns.SYNC_KEY, mSyncKey);
843        values.put(AccountColumns.SYNC_LOOKBACK, mSyncLookback);
844        values.put(AccountColumns.SYNC_INTERVAL, mSyncInterval);
845        values.put(AccountColumns.HOST_AUTH_KEY_RECV, mHostAuthKeyRecv);
846        values.put(AccountColumns.HOST_AUTH_KEY_SEND, mHostAuthKeySend);
847        values.put(AccountColumns.FLAGS, mFlags);
848        values.put(AccountColumns.IS_DEFAULT, mIsDefault);
849        values.put(AccountColumns.COMPATIBILITY_UUID, mCompatibilityUuid);
850        values.put(AccountColumns.SENDER_NAME, mSenderName);
851        values.put(AccountColumns.RINGTONE_URI, mRingtoneUri);
852        values.put(AccountColumns.PROTOCOL_VERSION, mProtocolVersion);
853        values.put(AccountColumns.NEW_MESSAGE_COUNT, mNewMessageCount);
854        values.put(AccountColumns.SECURITY_SYNC_KEY, mSecuritySyncKey);
855        values.put(AccountColumns.SIGNATURE, mSignature);
856        values.put(AccountColumns.POLICY_KEY, mPolicyKey);
857        values.put(AccountColumns.NOTIFIED_MESSAGE_ID, mNotifiedMessageId);
858        values.put(AccountColumns.NOTIFIED_MESSAGE_COUNT, mNotifiedMessageCount);
859        return values;
860    }
861
862    /**
863     * Supports Parcelable
864     */
865    @Override
866    public int describeContents() {
867        return 0;
868    }
869
870    /**
871     * Supports Parcelable
872     */
873    public static final Parcelable.Creator<Account> CREATOR
874            = new Parcelable.Creator<Account>() {
875        @Override
876        public Account createFromParcel(Parcel in) {
877            return new Account(in);
878        }
879
880        @Override
881        public Account[] newArray(int size) {
882            return new Account[size];
883        }
884    };
885
886    /**
887     * Supports Parcelable
888     */
889    @Override
890    public void writeToParcel(Parcel dest, int flags) {
891        // mBaseUri is not parceled
892        dest.writeLong(mId);
893        dest.writeString(mDisplayName);
894        dest.writeString(mEmailAddress);
895        dest.writeString(mSyncKey);
896        dest.writeInt(mSyncLookback);
897        dest.writeInt(mSyncInterval);
898        dest.writeLong(mHostAuthKeyRecv);
899        dest.writeLong(mHostAuthKeySend);
900        dest.writeInt(mFlags);
901        dest.writeByte(mIsDefault ? (byte)1 : (byte)0);
902        dest.writeString(mCompatibilityUuid);
903        dest.writeString(mSenderName);
904        dest.writeString(mRingtoneUri);
905        dest.writeString(mProtocolVersion);
906        dest.writeInt(mNewMessageCount);
907        dest.writeString(mSecuritySyncKey);
908        dest.writeString(mSignature);
909        dest.writeLong(mPolicyKey);
910
911        if (mHostAuthRecv != null) {
912            dest.writeByte((byte)1);
913            mHostAuthRecv.writeToParcel(dest, flags);
914        } else {
915            dest.writeByte((byte)0);
916        }
917
918        if (mHostAuthSend != null) {
919            dest.writeByte((byte)1);
920            mHostAuthSend.writeToParcel(dest, flags);
921        } else {
922            dest.writeByte((byte)0);
923        }
924    }
925
926    /**
927     * Supports Parcelable
928     */
929    public Account(Parcel in) {
930        mBaseUri = Account.CONTENT_URI;
931        mId = in.readLong();
932        mDisplayName = in.readString();
933        mEmailAddress = in.readString();
934        mSyncKey = in.readString();
935        mSyncLookback = in.readInt();
936        mSyncInterval = in.readInt();
937        mHostAuthKeyRecv = in.readLong();
938        mHostAuthKeySend = in.readLong();
939        mFlags = in.readInt();
940        mIsDefault = in.readByte() == 1;
941        mCompatibilityUuid = in.readString();
942        mSenderName = in.readString();
943        mRingtoneUri = in.readString();
944        mProtocolVersion = in.readString();
945        mNewMessageCount = in.readInt();
946        mSecuritySyncKey = in.readString();
947        mSignature = in.readString();
948        mPolicyKey = in.readLong();
949
950        mHostAuthRecv = null;
951        if (in.readByte() == 1) {
952            mHostAuthRecv = new HostAuth(in);
953        }
954
955        mHostAuthSend = null;
956        if (in.readByte() == 1) {
957            mHostAuthSend = new HostAuth(in);
958        }
959    }
960
961    /**
962     * For debugger support only - DO NOT use for code.
963     */
964    @Override
965    public String toString() {
966        StringBuilder sb = new StringBuilder('[');
967        if (mHostAuthRecv != null && mHostAuthRecv.mProtocol != null) {
968            sb.append(mHostAuthRecv.mProtocol);
969            sb.append(':');
970        }
971        if (mDisplayName != null)   sb.append(mDisplayName);
972        sb.append(':');
973        if (mEmailAddress != null)  sb.append(mEmailAddress);
974        sb.append(':');
975        if (mSenderName != null)    sb.append(mSenderName);
976        sb.append(']');
977        return sb.toString();
978    }
979}