1/*
2 * Copyright (C) 2010 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.email;
18
19import com.android.email.mail.store.ExchangeStore;
20import com.android.email.provider.EmailContent;
21
22import android.accounts.AccountManagerFuture;
23import android.content.ContentResolver;
24import android.content.Context;
25import android.database.Cursor;
26import android.os.Bundle;
27import android.provider.Calendar;
28import android.provider.ContactsContract;
29import android.util.Log;
30
31/**
32 * Utility functions to support backup and restore of accounts.
33 *
34 * In the short term, this is used to work around local database failures.  In the long term,
35 * this will also support server-side backups, providing support for automatic account restoration
36 * when switching or replacing phones.
37 */
38public class AccountBackupRestore {
39
40    /**
41     * Backup accounts.  Can be called from UI thread (does work in a new thread)
42     */
43    public static void backupAccounts(final Context context) {
44        if (Email.DEBUG) {
45            Log.v(Email.LOG_TAG, "backupAccounts");
46        }
47        // Because we typically call this from the UI, let's do the work in a thread
48        new Thread() {
49            @Override
50            public void run() {
51                doBackupAccounts(context, Preferences.getPreferences(context));
52            }
53        }.start();
54    }
55
56    /**
57     * Restore accounts if needed.  This is blocking, and should only be called in specific
58     * startup/entry points.
59     */
60    public static void restoreAccountsIfNeeded(final Context context) {
61        // Don't log here;  This is called often.
62        boolean restored = doRestoreAccounts(context, Preferences.getPreferences(context));
63        if (restored) {
64            // after restoring accounts, register services appropriately
65            Log.w(Email.LOG_TAG, "Register services after restoring accounts");
66            // update security profile
67            SecurityPolicy.getInstance(context).updatePolicies(-1);
68            // enable/disable other email services as necessary
69            Email.setServicesEnabled(context);
70            ExchangeUtils.startExchangeService(context);
71        }
72    }
73
74    /**
75     * Non-UI-Thread worker to backup all accounts
76     *
77     * @param context used to access the provider
78     * @param preferences used to access the backups (provided separately for testability)
79     */
80    /* package */ synchronized static void doBackupAccounts(Context context,
81            Preferences preferences) {
82        // 1.  Wipe any existing backup accounts
83        Account[] oldBackups = preferences.getAccounts();
84        for (Account backup : oldBackups) {
85            backup.delete(preferences);
86        }
87
88        // 2. Identify the default account (if any).  This is required because setting
89        // the default account flag is lazy,and sometimes we don't have any flags set.  We'll
90        // use this to make it explicit (see loop, below).
91        // This is also the quick check for "no accounts" (the only case in which the returned
92        // value is -1) and if so, we can exit immediately.
93        long defaultAccountId = EmailContent.Account.getDefaultAccountId(context);
94        if (defaultAccountId == -1) {
95            return;
96        }
97
98        // 3. Create new backup(s), if any
99        Cursor c = context.getContentResolver().query(EmailContent.Account.CONTENT_URI,
100                EmailContent.Account.CONTENT_PROJECTION, null, null, null);
101        try {
102            while (c.moveToNext()) {
103                EmailContent.Account fromAccount =
104                        EmailContent.getContent(c, EmailContent.Account.class);
105                if (Email.DEBUG) {
106                    Log.v(Email.LOG_TAG, "Backing up account:" + fromAccount.getDisplayName());
107                }
108                Account toAccount = LegacyConversions.makeLegacyAccount(context, fromAccount);
109
110                // Determine if contacts are also synced, and if so, record that
111                if (fromAccount.mHostAuthRecv.mProtocol.equals("eas")) {
112                    android.accounts.Account acct = new android.accounts.Account(
113                            fromAccount.mEmailAddress, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
114                    boolean syncContacts = ContentResolver.getSyncAutomatically(acct,
115                            ContactsContract.AUTHORITY);
116                    if (syncContacts) {
117                        toAccount.mBackupFlags |= Account.BACKUP_FLAGS_SYNC_CONTACTS;
118                    }
119                    boolean syncCalendar = ContentResolver.getSyncAutomatically(acct,
120                            Calendar.AUTHORITY);
121                    if (syncCalendar) {
122                        toAccount.mBackupFlags |= Account.BACKUP_FLAGS_SYNC_CALENDAR;
123                    }
124                }
125
126                // If this is the default account, mark it as such
127                if (fromAccount.mId == defaultAccountId) {
128                    toAccount.mBackupFlags |= Account.BACKUP_FLAGS_IS_DEFAULT;
129                }
130
131                // Mark this account as a backup of a Provider account, instead of a legacy
132                // account to upgrade
133                toAccount.mBackupFlags |= Account.BACKUP_FLAGS_IS_BACKUP;
134
135                toAccount.save(preferences);
136            }
137        } finally {
138            c.close();
139        }
140    }
141
142    /**
143     * Restore all accounts.  This is blocking.
144     *
145     * @param context used to access the provider
146     * @param preferences used to access the backups (provided separately for testability)
147     * @return true if accounts were restored (meaning services should be restarted, etc.)
148     */
149    /* package */ synchronized static boolean doRestoreAccounts(Context context,
150            Preferences preferences) {
151        boolean result = false;
152
153        // 1. Quick check - if we have any accounts, get out
154        int numAccounts = EmailContent.count(context, EmailContent.Account.CONTENT_URI, null, null);
155        if (numAccounts > 0) {
156            return result;
157        }
158        // 2. Quick check - if no backup accounts, get out
159        Account[] backups = preferences.getAccounts();
160        if (backups.length == 0) {
161            return result;
162        }
163
164        Log.w(Email.LOG_TAG, "*** Restoring Email Accounts, found " + backups.length);
165
166        // 3. Possible lost accounts situation - check for any backups, and restore them
167        for (Account backupAccount : backups) {
168            // don't back up any leftover legacy accounts (these are migrated elsewhere).
169            if ((backupAccount.mBackupFlags & Account.BACKUP_FLAGS_IS_BACKUP) == 0) {
170                continue;
171            }
172            // Restore the account
173            Log.w(Email.LOG_TAG, "Restoring account:" + backupAccount.getDescription());
174            EmailContent.Account toAccount =
175                LegacyConversions.makeAccount(context, backupAccount);
176
177            // Mark the default account if this is it
178            if (0 != (backupAccount.mBackupFlags & Account.BACKUP_FLAGS_IS_DEFAULT)) {
179                toAccount.setDefaultAccount(true);
180            }
181
182            // For exchange accounts, handle system account first, then save in provider
183            if (toAccount.mHostAuthRecv.mProtocol.equals("eas")) {
184                // Recreate entry in Account Manager as well, if needed
185                // Set "sync contacts/calendar" mode as well, if needed
186                boolean alsoSyncContacts =
187                    (backupAccount.mBackupFlags & Account.BACKUP_FLAGS_SYNC_CONTACTS) != 0;
188                boolean alsoSyncCalendar =
189                    (backupAccount.mBackupFlags & Account.BACKUP_FLAGS_SYNC_CALENDAR) != 0;
190
191                // Use delete-then-add semantic to simplify handling of update-in-place
192//                AccountManagerFuture<Boolean> removeResult = ExchangeStore.removeSystemAccount(
193//                        context.getApplicationContext(), toAccount, null);
194//                try {
195//                    // This call blocks until removeSystemAccount completes.  Result is not used.
196//                    removeResult.getResult();
197//                } catch (AccountsException e) {
198//                    Log.d(Email.LOG_TAG, "removeSystemAccount failed: " + e);
199//                    // log and discard - we don't care if remove fails, generally
200//                } catch (IOException e) {
201//                    Log.d(Email.LOG_TAG, "removeSystemAccount failed: " + e);
202//                    // log and discard - we don't care if remove fails, generally
203//                }
204
205                // NOTE: We must use the Application here, rather than the current context, because
206                // all future references to AccountManager will use the context passed in here
207                // TODO: Need to implement overwrite semantics for an already-installed account
208                AccountManagerFuture<Bundle> addAccountResult =
209                     ExchangeStore.addSystemAccount(context.getApplicationContext(), toAccount,
210                             alsoSyncContacts, alsoSyncCalendar, null);
211//                try {
212//                    // This call blocks until addSystemAccount completes.  Result is not used.
213//                    addAccountResult.getResult();
214                    toAccount.save(context);
215//                } catch (OperationCanceledException e) {
216//                    Log.d(Email.LOG_TAG, "addAccount was canceled");
217//                } catch (IOException e) {
218//                    Log.d(Email.LOG_TAG, "addAccount failed: " + e);
219//                } catch (AuthenticatorException e) {
220//                    Log.d(Email.LOG_TAG, "addAccount failed: " + e);
221//                }
222
223            } else {
224                // non-eas account - save it immediately
225                toAccount.save(context);
226            }
227            // report that an account was restored
228            result = true;
229        }
230        return result;
231    }
232}
233