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