EmailSyncAdapterService.java revision 328ca0d959f7e729e96f19538c8f3af8fe782a09
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.exchange.service;
18
19import android.content.AbstractThreadedSyncAdapter;
20import android.content.ContentProviderClient;
21import android.content.ContentResolver;
22import android.content.ContentValues;
23import android.content.Context;
24import android.content.Intent;
25import android.content.SyncResult;
26import android.database.Cursor;
27import android.os.AsyncTask;
28import android.os.Bundle;
29import android.os.IBinder;
30
31import com.android.emailcommon.Api;
32import com.android.emailcommon.TempDirectory;
33import com.android.emailcommon.provider.Account;
34import com.android.emailcommon.provider.EmailContent;
35import com.android.emailcommon.provider.EmailContent.AccountColumns;
36import com.android.emailcommon.provider.HostAuth;
37import com.android.emailcommon.provider.Mailbox;
38import com.android.emailcommon.service.IEmailService;
39import com.android.emailcommon.service.IEmailServiceCallback;
40import com.android.emailcommon.service.SearchParams;
41import com.android.emailcommon.utility.Utility;
42import com.android.exchange.Eas;
43import com.android.exchange.adapter.PingParser;
44import com.android.exchange.adapter.Search;
45import com.android.exchange.eas.EasFolderSync;
46import com.android.exchange.eas.EasOperation;
47import com.android.exchange.eas.EasPing;
48import com.android.mail.providers.UIProvider.AccountCapabilities;
49import com.android.mail.utils.LogUtils;
50
51import java.util.HashMap;
52
53/**
54 * Service for communicating with Exchange servers. There are three main parts of this class:
55 * TODO: Flesh out these comments.
56 * 1) An {@link AbstractThreadedSyncAdapter} to handle actually performing syncs.
57 * 2) Bookkeeping for running Ping requests, which handles push notifications.
58 * 3) An {@link IEmailService} Stub to handle RPC from the UI.
59 */
60public class EmailSyncAdapterService extends AbstractSyncAdapterService {
61
62    private static final String TAG = "EASEmailSyncAdaptSvc";
63
64    /**
65     * If sync extras do not include a mailbox id, then we want to perform a full sync.
66     */
67    private static final long FULL_ACCOUNT_SYNC = Mailbox.NO_MAILBOX;
68
69    /** Projection used for getting email address for an account. */
70    private static final String[] ACCOUNT_EMAIL_PROJECTION = { AccountColumns.EMAIL_ADDRESS };
71
72    /**
73     * Bookkeeping for handling synchronization between pings and syncs.
74     * "Ping" refers to a hanging POST or GET that is used to receive push notifications. Ping is
75     * the term for the Exchange command, but this code should be generic enough to be easily
76     * extended to IMAP.
77     * "Sync" refers to an actual sync command to either fetch mail state, account state, or send
78     * mail (send is implemented as "sync the outbox").
79     * TODO: Outbox sync probably need not stop a ping in progress.
80     * Basic rules of how these interact (note that all rules are per account):
81     * - Only one ping or sync may run at a time.
82     * - Due to how {@link AbstractThreadedSyncAdapter} works, sync requests will not occur while
83     *   a sync is in progress.
84     * - On the other hand, ping requests may come in while handling a ping.
85     * - "Ping request" is shorthand for "a request to change our ping parameters", which includes
86     *   a request to stop receiving push notifications.
87     * - If neither a ping nor a sync is running, then a request for either will run it.
88     * - If a sync is running, new ping requests block until the sync completes.
89     * - If a ping is running, a new sync request stops the ping and creates a pending ping
90     *   (which blocks until the sync completes).
91     * - If a ping is running, a new ping request stops the ping and either starts a new one or
92     *   does nothing, as appopriate (since a ping request can be to stop pushing).
93     * - As an optimization, while a ping request is waiting to run, subsequent ping requests are
94     *   ignored (the pending ping will pick up the latest ping parameters at the time it runs).
95     */
96    public class SyncHandlerSynchronizer {
97        /**
98         * Map of account id -> ping handler.
99         * For a given account id, there are three possible states:
100         * 1) If no ping or sync is currently running, there is no entry in the map for the account.
101         * 2) If a ping is running, there is an entry with the appropriate ping handler.
102         * 3) If there is a sync running, there is an entry with null as the value.
103         * We cannot have more than one ping or sync running at a time.
104         */
105        private final HashMap<Long, PingTask> mPingHandlers = new HashMap<Long, PingTask>();
106
107        /**
108         * Wait until neither a sync nor a ping is running on this account, and then return.
109         * If there's a ping running, actively stop it. (For syncs, we have to just wait.)
110         * @param accountId The account we want to wait for.
111         */
112        private synchronized void waitUntilNoActivity(final long accountId) {
113            while (mPingHandlers.containsKey(accountId)) {
114                final PingTask pingHandler = mPingHandlers.get(accountId);
115                if (pingHandler != null) {
116                    pingHandler.stop();
117                }
118                try {
119                    wait();
120                } catch (final InterruptedException e) {
121                    // TODO: When would this happen, and how should I handle it?
122                }
123            }
124        }
125
126        /**
127         * Use this to see if we're currently syncing, as opposed to pinging or doing nothing.
128         * @param accountId The account to check.
129         * @return Whether that account is currently running a sync.
130         */
131        private synchronized boolean isRunningSync(final long accountId) {
132            return (mPingHandlers.containsKey(accountId) && mPingHandlers.get(accountId) == null);
133        }
134
135        /**
136         * If there are no running pings, stop the service.
137         */
138        private void stopServiceIfNoPings() {
139            for (final PingTask pingHandler : mPingHandlers.values()) {
140                if (pingHandler != null) {
141                    return;
142                }
143            }
144            EmailSyncAdapterService.this.stopSelf();
145        }
146
147        /**
148         * Called prior to starting a sync to update our bookkeeping. We don't actually run the sync
149         * here; the caller must do that.
150         * @param accountId The account on which we are running a sync.
151         */
152        public synchronized void startSync(final long accountId) {
153            waitUntilNoActivity(accountId);
154            mPingHandlers.put(accountId, null);
155        }
156
157        /**
158         * Starts or restarts a ping for an account, if the current account state indicates that it
159         * wants to push.
160         * @param account The account whose ping is being modified.
161         */
162        public synchronized void modifyPing(final Account account) {
163            // If a sync is currently running, it will start a ping when it's done, so there's no
164            // need to do anything right now.
165            if (isRunningSync(account.mId)) {
166                return;
167            }
168
169            // If a ping is currently running, tell it to restart to pick up new params.
170            final PingTask pingSyncHandler = mPingHandlers.get(account.mId);
171            if (pingSyncHandler != null) {
172                pingSyncHandler.restart();
173                return;
174            }
175
176            // If we're here, then there's neither a sync nor a ping running. Start a new ping.
177            final EmailSyncAdapterService service = EmailSyncAdapterService.this;
178            if (account.mSyncInterval == Account.CHECK_INTERVAL_PUSH) {
179                // TODO: Also check if we have any mailboxes that WANT push.
180                // This account needs to ping.
181                // Note: unlike startSync, we CANNOT allow the caller to do the actual work.
182                // If we return before the ping starts, there's a race condition where another
183                // ping or sync might start first. It only works for startSync because sync is
184                // higher priority than ping (i.e. a ping can't start while a sync is pending)
185                // and only one sync can run at a time.
186                final PingTask pingHandler = new PingTask(service, account, this);
187                mPingHandlers.put(account.mId, pingHandler);
188                pingHandler.start();
189                // Whenever we have a running ping, make sure this service stays running.
190                service.startService(new Intent(service, EmailSyncAdapterService.class));
191            }
192        }
193
194        /**
195         * Updates the synchronization bookkeeping when a sync is done.
196         * @param account The account whose sync just finished.
197         */
198        public synchronized void syncComplete(final Account account) {
199            mPingHandlers.remove(account.mId);
200            // Syncs can interrupt pings, so we should check if we need to start one now.
201            modifyPing(account);
202            stopServiceIfNoPings();
203            notifyAll();
204        }
205
206        /**
207         * Updates the synchronization bookkeeping when a ping is done. Also requests a ping-only
208         * sync if necessary.
209         * @param amAccount The {@link android.accounts.Account} for this account.
210         * @param accountId The account whose ping just finished.
211         * @param pingStatus The status value from {@link PingParser} for the last ping performed.
212         *                   This cannot be one of the values that results in another ping, so this
213         *                   function only needs to handle the terminal statuses.
214         */
215        public synchronized void pingComplete(final android.accounts.Account amAccount,
216                final long accountId, final int pingStatus) {
217            mPingHandlers.remove(accountId);
218
219            // TODO: if (pingStatus == PingParser.STATUS_FAILED), notify UI.
220            // TODO: if (pingStatus == PingParser.STATUS_REQUEST_TOO_MANY_FOLDERS), notify UI.
221
222            if (pingStatus == EasOperation.RESULT_REQUEST_FAILURE) {
223                // Request a new ping through the SyncManager. This will do the right thing if the
224                // exception was due to loss of network connectivity, etc. (i.e. it will wait for
225                // network to restore and then request it).
226                EasPing.requestPing(amAccount);
227            } else {
228                stopServiceIfNoPings();
229            }
230
231            // TODO: It might be the case that only STATUS_CHANGES_FOUND and
232            // STATUS_FOLDER_REFRESH_NEEDED need to notifyAll(). Think this through.
233            notifyAll();
234        }
235
236    }
237    private final SyncHandlerSynchronizer mSyncHandlerMap = new SyncHandlerSynchronizer();
238
239    /**
240     * The binder for IEmailService.
241     */
242    private final IEmailService.Stub mBinder = new IEmailService.Stub() {
243
244        private String getEmailAddressForAccount(final long accountId) {
245            final String emailAddress = Utility.getFirstRowString(EmailSyncAdapterService.this,
246                    Account.CONTENT_URI, ACCOUNT_EMAIL_PROJECTION, Account.ID_SELECTION,
247                    new String[] {Long.toString(accountId)}, null, 0);
248            if (emailAddress == null) {
249                LogUtils.e(TAG, "Could not find email address for account %d", accountId);
250            }
251            return emailAddress;
252        }
253
254        @Override
255        public Bundle validate(final HostAuth hostAuth) {
256            LogUtils.d(TAG, "IEmailService.validate");
257            return new EasAccountValidator(EmailSyncAdapterService.this, hostAuth).validate();
258        }
259
260        @Override
261        public Bundle autoDiscover(final String username, final String password) {
262            LogUtils.d(TAG, "IEmailService.autoDiscover");
263            return new EasAutoDiscover(EmailSyncAdapterService.this, username, password)
264                    .doAutodiscover();
265        }
266
267        @Override
268        public void updateFolderList(final long accountId) {
269            LogUtils.d(TAG, "IEmailService.updateFolderList: %d", accountId);
270            final String emailAddress = getEmailAddressForAccount(accountId);
271            if (emailAddress != null) {
272                ContentResolver.requestSync(new android.accounts.Account(
273                        emailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
274                        EmailContent.AUTHORITY, new Bundle());
275            }
276        }
277
278        @Override
279        public void setLogging(final int flags) {
280            // TODO: fix this?
281            // Protocol logging
282            Eas.setUserDebug(flags);
283            // Sync logging
284            //setUserDebug(flags);
285        }
286
287        @Override
288        public void loadAttachment(final IEmailServiceCallback callback, final long attachmentId,
289                final boolean background) {
290            LogUtils.d(TAG, "IEmailService.loadAttachment: %d", attachmentId);
291            // TODO: Prevent this from happening in parallel with a sync?
292            EasAttachmentLoader.loadAttachment(EmailSyncAdapterService.this, attachmentId,
293                    callback);
294        }
295
296        @Override
297        public void sendMeetingResponse(final long messageId, final int response) {
298            LogUtils.d(TAG, "IEmailService.sendMeetingResponse: %d, %d", messageId, response);
299            EasMeetingResponder.sendMeetingResponse(EmailSyncAdapterService.this, messageId,
300                    response);
301        }
302
303        /**
304         * Delete PIM (calendar, contacts) data for the specified account
305         *
306         * @param emailAddress the email address for the account whose data should be deleted
307         */
308        @Override
309        public void deleteAccountPIMData(final String emailAddress) {
310            LogUtils.d(TAG, "IEmailService.deleteAccountPIMData");
311            if (emailAddress != null) {
312                final Context context = EmailSyncAdapterService.this;
313                EasContactsSyncHandler.wipeAccountFromContentProvider(context, emailAddress);
314                EasCalendarSyncHandler.wipeAccountFromContentProvider(context, emailAddress);
315            }
316            // TODO: Run account reconciler?
317        }
318
319        @Override
320        public int searchMessages(final long accountId, final SearchParams searchParams,
321                final long destMailboxId) {
322            LogUtils.d(TAG, "IEmailService.searchMessages");
323            return Search.searchMessages(EmailSyncAdapterService.this, accountId, searchParams,
324                    destMailboxId);
325            // TODO: may need an explicit callback to replace the one to IEmailServiceCallback.
326        }
327
328        @Override
329        public void sendMail(final long accountId) {}
330
331        @Override
332        public int getCapabilities(final Account acct) {
333            String easVersion = acct.mProtocolVersion;
334            Double easVersionDouble = 2.5D;
335            if (easVersion != null) {
336                try {
337                    easVersionDouble = Double.parseDouble(easVersion);
338                } catch (NumberFormatException e) {
339                    // Stick with 2.5
340                }
341            }
342            if (easVersionDouble >= 12.0D) {
343                return AccountCapabilities.SYNCABLE_FOLDERS |
344                        AccountCapabilities.SERVER_SEARCH |
345                        AccountCapabilities.FOLDER_SERVER_SEARCH |
346                        AccountCapabilities.SANITIZED_HTML |
347                        AccountCapabilities.SMART_REPLY |
348                        AccountCapabilities.SERVER_SEARCH |
349                        AccountCapabilities.UNDO |
350                        AccountCapabilities.DISCARD_CONVERSATION_DRAFTS;
351            } else {
352                return AccountCapabilities.SYNCABLE_FOLDERS |
353                        AccountCapabilities.SANITIZED_HTML |
354                        AccountCapabilities.SMART_REPLY |
355                        AccountCapabilities.UNDO |
356                        AccountCapabilities.DISCARD_CONVERSATION_DRAFTS;
357            }
358        }
359
360        @Override
361        public void serviceUpdated(final String emailAddress) {
362            // Not required for EAS
363        }
364
365        // All IEmailService messages below are UNCALLED in Email.
366        // TODO: Remove.
367        @Deprecated
368        @Override
369        public int getApiLevel() {
370            return Api.LEVEL;
371        }
372
373        @Deprecated
374        @Override
375        public void startSync(long mailboxId, boolean userRequest, int deltaMessageCount) {}
376
377        @Deprecated
378        @Override
379        public void stopSync(long mailboxId) {}
380
381        @Deprecated
382        @Override
383        public void loadMore(long messageId) {}
384
385        @Deprecated
386        @Override
387        public boolean createFolder(long accountId, String name) {
388            return false;
389        }
390
391        @Deprecated
392        @Override
393        public boolean deleteFolder(long accountId, String name) {
394            return false;
395        }
396
397        @Deprecated
398        @Override
399        public boolean renameFolder(long accountId, String oldName, String newName) {
400            return false;
401        }
402
403        @Deprecated
404        @Override
405        public void hostChanged(long accountId) {}
406    };
407
408    public EmailSyncAdapterService() {
409        super();
410    }
411
412    /**
413     * {@link AsyncTask} for restarting pings for all accounts that need it.
414     */
415    private static class RestartPingsTask extends AsyncTask<Void, Void, Void> {
416        private static final String PUSH_ACCOUNTS_SELECTION =
417                AccountColumns.SYNC_INTERVAL + "=" + Integer.toString(Account.CHECK_INTERVAL_PUSH);
418
419        private final ContentResolver mContentResolver;
420        private final SyncHandlerSynchronizer mSyncHandlerMap;
421
422        public RestartPingsTask(final ContentResolver contentResolver,
423                final SyncHandlerSynchronizer syncHandlerMap) {
424            mContentResolver = contentResolver;
425            mSyncHandlerMap = syncHandlerMap;
426        }
427
428        @Override
429        protected Void doInBackground(Void... params) {
430            final Cursor c = mContentResolver.query(Account.CONTENT_URI,
431                    Account.CONTENT_PROJECTION, PUSH_ACCOUNTS_SELECTION, null, null);
432            if (c != null) {
433                try {
434                    while (c.moveToNext()) {
435                        final Account account = new Account();
436                        account.restore(c);
437                        mSyncHandlerMap.modifyPing(account);
438                    }
439                } finally {
440                    c.close();
441                }
442            }
443            return null;
444        }
445    }
446
447    @Override
448    public void onCreate() {
449        super.onCreate();
450        // Restart push for all accounts that need it.
451        new RestartPingsTask(getContentResolver(), mSyncHandlerMap).executeOnExecutor(
452                AsyncTask.THREAD_POOL_EXECUTOR);
453    }
454
455    @Override
456    public IBinder onBind(Intent intent) {
457        if (intent.getAction().equals(Eas.EXCHANGE_SERVICE_INTENT_ACTION)) {
458            return mBinder;
459        }
460        return super.onBind(intent);
461    }
462
463    @Override
464    protected AbstractThreadedSyncAdapter newSyncAdapter() {
465        return new SyncAdapterImpl(this);
466    }
467
468    // TODO: Handle cancelSync() appropriately.
469    private class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
470        public SyncAdapterImpl(Context context) {
471            super(context, true /* autoInitialize */);
472        }
473
474        @Override
475        public void onPerformSync(final android.accounts.Account acct, final Bundle extras,
476                final String authority, final ContentProviderClient provider,
477                final SyncResult syncResult) {
478            LogUtils.i(TAG, "performSync: extras = %s", extras.toString());
479            TempDirectory.setTempDirectory(EmailSyncAdapterService.this);
480
481            // TODO: Perform any connectivity checks, bail early if we don't have proper network
482            // for this sync operation.
483
484            final Context context = getContext();
485            final ContentResolver cr = context.getContentResolver();
486
487            // Get the EmailContent Account
488            final Account account;
489            final Cursor accountCursor = cr.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION,
490                    AccountColumns.EMAIL_ADDRESS + "=?", new String[] {acct.name}, null);
491            try {
492                if (!accountCursor.moveToFirst()) {
493                    // Could not load account.
494                    // TODO: improve error handling.
495                    return;
496                }
497                account = new Account();
498                account.restore(accountCursor);
499            } finally {
500                accountCursor.close();
501            }
502            // Get the mailbox that we want to sync.
503            // There are four possibilities for Mailbox.SYNC_EXTRA_MAILBOX_ID:
504            // 1) Mailbox.SYNC_EXTRA_MAILBOX_ID_PUSH_ONLY: Restart push if appropriate.
505            // 2) Mailbox.SYNC_EXTRA_MAILBOX_ID_ACCOUNT_ONLY: Sync only the account data.
506            // 3) Not present: Perform a full account sync.
507            // 4) Non-negative value: It's an actual mailbox id, sync that mailbox only.
508            final long mailboxId = extras.getLong(Mailbox.SYNC_EXTRA_MAILBOX_ID, FULL_ACCOUNT_SYNC);
509
510            // If we're just twiddling the push, we do the lightweight thing and just bail.
511            if (mailboxId == Mailbox.SYNC_EXTRA_MAILBOX_ID_PUSH_ONLY) {
512                mSyncHandlerMap.modifyPing(account);
513                return;
514            }
515
516            // Do the bookkeeping for starting a sync, including stopping a ping if necessary.
517            mSyncHandlerMap.startSync(account.mId);
518
519            // TODO: Should we refresh the account here? It may have changed while waiting for any
520            // pings to stop. It may not matter since the things that may have been twiddled might
521            // not affect syncing.
522
523            if (mailboxId == FULL_ACCOUNT_SYNC ||
524                    mailboxId == Mailbox.SYNC_EXTRA_MAILBOX_ID_ACCOUNT_ONLY) {
525                final EasFolderSync folderSync = new EasFolderSync(context, account);
526                folderSync.doFolderSync(syncResult);
527
528                if (mailboxId == FULL_ACCOUNT_SYNC) {
529                    // Full account sync includes all mailboxes that participate in system sync.
530                    final Cursor c = Mailbox.getMailboxIdsForSync(cr, account.mId);
531                    if (c != null) {
532                        try {
533                            while (c.moveToNext()) {
534                                syncMailbox(context, cr, acct, account, c.getLong(0), extras,
535                                        syncResult, false);
536                            }
537                        } finally {
538                            c.close();
539                        }
540                    }
541                }
542            } else {
543                // Sync the mailbox that was explicitly requested.
544                syncMailbox(context, cr, acct, account, mailboxId, extras, syncResult, true);
545            }
546
547            // Clean up the bookkeeping, including restarting ping if necessary.
548            mSyncHandlerMap.syncComplete(account);
549
550            // TODO: It may make sense to have common error handling here. Two possible mechanisms:
551            // 1) performSync return value can signal some useful info.
552            // 2) syncResult can contain useful info.
553        }
554
555        /**
556         * Update the mailbox's sync status with the provider and, if we're finished with the sync,
557         * write the last sync time as well.
558         * @param context Our {@link Context}.
559         * @param mailbox The mailbox whose sync status to update.
560         * @param cv A {@link ContentValues} object to use for updating the provider.
561         * @param syncStatus The status for the current sync.
562         */
563        private void updateMailbox(final Context context, final Mailbox mailbox,
564                final ContentValues cv, final int syncStatus) {
565            cv.put(Mailbox.UI_SYNC_STATUS, syncStatus);
566            if (syncStatus == EmailContent.SYNC_STATUS_NONE) {
567                cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
568            }
569            mailbox.update(context, cv);
570        }
571
572        private boolean syncMailbox(final Context context, final ContentResolver cr,
573                final android.accounts.Account acct, final Account account, final long mailboxId,
574                final Bundle extras, final SyncResult syncResult, final boolean isMailboxSync) {
575            final Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
576            if (mailbox == null) {
577                return false;
578            }
579
580            final boolean success;
581            // Non-mailbox syncs are whole account syncs initiated by the AccountManager and are
582            // treated as background syncs.
583            // TODO: Push will be treated as "user" syncs, and probably should be background.
584            final ContentValues cv = new ContentValues(2);
585            updateMailbox(context, mailbox, cv, isMailboxSync ?
586                    EmailContent.SYNC_STATUS_USER : EmailContent.SYNC_STATUS_BACKGROUND);
587
588            if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
589                final EasOutboxSyncHandler outboxSyncHandler =
590                        new EasOutboxSyncHandler(context, account, mailbox);
591                outboxSyncHandler.performSync();
592                success = true;
593            } else if(mailbox.isSyncable()) {
594                final EasSyncHandler syncHandler = EasSyncHandler.getEasSyncHandler(context, cr,
595                        acct, account, mailbox, extras, syncResult);
596                success = (syncHandler != null);
597                if (syncHandler != null) {
598                    syncHandler.performSync();
599                }
600            } else {
601                success = false;
602            }
603            updateMailbox(context, mailbox, cv, EmailContent.SYNC_STATUS_NONE);
604            return success;
605        }
606    }
607}
608