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