EmailSyncAdapterService.java revision 456a6e3e01205e8c779930d8c7533b1c7467df5e
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.Context;
23import android.content.Intent;
24import android.content.SyncResult;
25import android.database.Cursor;
26import android.os.Bundle;
27import android.os.IBinder;
28
29import com.android.emailcommon.Api;
30import com.android.emailcommon.TempDirectory;
31import com.android.emailcommon.provider.Account;
32import com.android.emailcommon.provider.EmailContent;
33import com.android.emailcommon.provider.EmailContent.AccountColumns;
34import com.android.emailcommon.provider.HostAuth;
35import com.android.emailcommon.provider.Mailbox;
36import com.android.emailcommon.service.EmailServiceStatus;
37import com.android.emailcommon.service.IEmailService;
38import com.android.emailcommon.service.IEmailServiceCallback;
39import com.android.emailcommon.service.SearchParams;
40import com.android.emailcommon.utility.Utility;
41import com.android.exchange.Eas;
42import com.android.exchange.adapter.Search;
43import com.android.mail.providers.UIProvider.AccountCapabilities;
44import com.android.mail.utils.LogUtils;
45
46import java.util.HashMap;
47import java.util.HashSet;
48
49/**
50 * Service for communicating with Exchange servers. There are three main parts of this class:
51 * TODO: Flesh out these comments.
52 * 1) An {@link AbstractThreadedSyncAdapter} to handle actually performing syncs.
53 * 2) Bookkeeping for running Ping requests, which handles push notifications.
54 * 3) An {@link IEmailService} Stub to handle RPC from the UI.
55 */
56public class EmailSyncAdapterService extends AbstractSyncAdapterService {
57
58    private static final String TAG = "EAS EmailSyncAdaptSvc";
59
60    /**
61     * If sync extras do not include a mailbox id, then we want to perform a full sync.
62     */
63    private static final long FULL_ACCOUNT_SYNC = Mailbox.NO_MAILBOX;
64
65    /**
66     * Bookkeeping for handling synchronization between pings and syncs.
67     * "Ping" refers to a hanging POST or GET that is used to receive push notifications. Ping is
68     * the term for the Exchange command, but this code should be generic enough to be easily
69     * extended to IMAP.
70     * "Sync" refers to an actual sync command to either fetch mail state, account state, or send
71     * mail (send is implemented as "sync the outbox").
72     * TODO: Outbox sync probably need not stop a ping in progress.
73     * Basic rules of how these interact (note that all rules are per account):
74     * - Only one ping or sync may run at a time.
75     * - Due to how {@link AbstractThreadedSyncAdapter} works, sync requests will not occur while
76     *   a sync is in progress.
77     * - On the other hand, ping requests may come in while handling a ping.
78     * - "Ping request" is shorthand for "a request to change our ping parameters", which includes
79     *   a request to stop receiving push notifications.
80     * - If neither a ping nor a sync is running, then a request for either will run it.
81     * - If a sync is running, new ping requests block until the sync completes.
82     * - If a ping is running, a new sync request stops the ping and creates a pending ping
83     *   (which blocks until the sync completes).
84     * - If a ping is running, a new ping request stops the ping and either starts a new one or
85     *   does nothing, as appopriate (since a ping request can be to stop pushing).
86     * - As an optimization, while a ping request is waiting to run, subsequent ping requests are
87     *   ignored (the pending ping will pick up the latest ping parameters at the time it runs).
88     */
89    public class SyncHandlerSynchronizer {
90        /**
91         * Map of account id -> ping handler.
92         * For a given account id, there are three possible states:
93         * 1) If no ping or sync is currently running, there is no entry in the map for the account.
94         * 2) If a ping is running, there is an entry with the appropriate ping handler.
95         * 3) If there is a sync running, there is an entry with null as the value.
96         * We cannot have more than one ping or sync running at a time.
97         */
98        private final HashMap<Long, EasPingSyncHandler> mPingHandlers =
99                new HashMap<Long, EasPingSyncHandler>();
100
101        /**
102         * Set of all accounts that are in the middle of processing a ping modification. This is
103         * used to ignore duplicate modification requests.
104         */
105        private final HashSet<Long> mPendingPings = new HashSet<Long>();
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 EasPingSyncHandler pingHandler = mPingHandlers.get(accountId);
115                if (pingHandler != null) {
116                    pingHandler.stop();
117                }
118                try {
119                    wait();
120                } catch (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        private void stopServiceIfNoPings() {
136            for (final EasPingSyncHandler pingHandler : mPingHandlers.values()) {
137                if (pingHandler != null) {
138                    return;
139                }
140            }
141            EmailSyncAdapterService.this.stopSelf();
142        }
143
144        /**
145         * Called prior to starting a sync to update our state.
146         * @param accountId The account on which we are running a sync.
147         */
148        public synchronized void startSync(final long accountId) {
149            waitUntilNoActivity(accountId);
150            mPingHandlers.put(accountId, null);
151        }
152
153        /**
154         * Called prior to starting, stopping, or changing a ping for reasons other than a sync
155         * request (e.g. new account added, settings change, or app startup). This is currently
156         * implemented as shutting down any running ping and starting a new one if needed. It might
157         * be better to signal any running ping to reload itself, but this is simpler for now.
158         * @param accountId The account whose ping is being modified.
159         */
160        public synchronized void modifyPing(final long accountId) {
161            // If a sync is currently running, we'd have to wait for it complete, but it'll call
162            // modifyPing at that point anyway. Therefore we can ignore this request.
163            if (isRunningSync(accountId)) {
164                return;
165            }
166
167            // Similarly, if multiple ping requests happen while a ping is running, we can ignore
168            // all but one of them -- by the time the first one is done waiting, it'll pick up the
169            // latest account settings anyway.
170            if (mPendingPings.contains(accountId)) {
171                return;
172            }
173            mPendingPings.add(accountId);
174
175            try {
176                // TODO: If a ping is running, it'd be better to just tell it to reload its state
177                // rather than kill and restart it.
178                waitUntilNoActivity(accountId);
179                final Context context = EmailSyncAdapterService.this;
180                // No ping or sync running. Figure out whether a ping is needed, and if so with
181                // what params.
182                final Account account = Account.restoreAccountWithId(context, accountId);
183                if (account == null || account.mSyncInterval != Account.CHECK_INTERVAL_PUSH) {
184                    // A ping that was running is no longer running, or something happened to the
185                    // account.
186                    stopServiceIfNoPings();
187                } else {
188                    // Note: unlike startSync, we CANNOT allow the caller to do the actual work.
189                    // If we return before the ping starts, there's a race condition where another
190                    // ping or sync might start first. It only works for startSync because sync is
191                    // higher priority than ping (i.e. a ping can't start while a sync is pending)
192                    // and only one ping can run at a time.
193                    final EasPingSyncHandler pingHandler =
194                            new EasPingSyncHandler(context, account, this);
195                    mPingHandlers.put(accountId, pingHandler);
196                    // Whenever we have a running ping, make sure this service stays running.
197                    final EmailSyncAdapterService service = EmailSyncAdapterService.this;
198                    service.startService(new Intent(service, EmailSyncAdapterService.class));
199                }
200            } finally {
201                mPendingPings.remove(accountId);
202            }
203        }
204
205        /**
206         * All operations must call this when they complete to update the synchronization
207         * bookkeeping.
208         * @param accountId The account whose ping or sync just completed.
209         * @param wasSync Whether the operation that's completing was a sync.
210         * @param notify Whether to notify all threads waiting on this object. This should be true
211         *     for all sync operations, and for any pings that were interrupted. Pings that complete
212         *     naturally possibly don't need to wake up anyone else.
213         *     TODO: is this optimization worth any possible problem? For example, the syncs started
214         *     by a ping may need to be signaled here.
215         */
216        public synchronized void signalDone(final long accountId, final boolean wasSync,
217                final boolean notify) {
218            mPingHandlers.remove(accountId);
219            // If this was a sync, we may have killed a ping that now needs to be restarted.
220            // modifyPing will do the appropriate checks.
221            // We do this here rather than at the caller because at this point, we are guaranteed
222            // that there is no entry for this account in mPingHandlers, and therefore we cannot
223            // block.
224            if (wasSync) {
225                modifyPing(accountId);
226            } else {
227                // A ping stopped, so check if we should stop the service.
228                stopServiceIfNoPings();
229            }
230
231            // Similarly, it's ok to notify after we restart the ping, because we know the ping
232            // can't possibly be waiting.
233            if (notify) {
234                notifyAll();
235            }
236        }
237    }
238    private final SyncHandlerSynchronizer mSyncHandlerMap = new SyncHandlerSynchronizer();
239
240    /**
241     * The binder for IEmailService.
242     */
243    private final IEmailService.Stub mBinder = new IEmailService.Stub() {
244        @Override
245        public Bundle validate(final HostAuth hostAuth) {
246            LogUtils.d(TAG, "IEmailService.validate");
247            return new EasAccountValidator(EmailSyncAdapterService.this, hostAuth).validate();
248        }
249
250        @Override
251        public Bundle autoDiscover(final String username, final String password) {
252            LogUtils.d(TAG, "IEmailService.autoDiscover");
253            return new EasAutoDiscover(EmailSyncAdapterService.this, username, password)
254                    .doAutodiscover();
255        }
256
257        @Override
258        public void updateFolderList(final long accountId) {
259            LogUtils.d(TAG, "IEmailService.updateFolderList");
260            final String emailAddress = Utility.getFirstRowString(EmailSyncAdapterService.this,
261                    Account.CONTENT_URI, new String[] {AccountColumns.EMAIL_ADDRESS},
262                    Account.ID_SELECTION, new String[] {Long.toString(accountId)}, null, 0);
263            if (emailAddress != null) {
264                ContentResolver.requestSync(new android.accounts.Account(
265                        emailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
266                        EmailContent.AUTHORITY, new Bundle());
267            }
268        }
269
270        @Override
271        public void setCallback(final IEmailServiceCallback cb) {
272            // TODO: Determine if this is ever called in practice.
273            //mCallbackList.register(cb);
274        }
275
276        @Override
277        public void setLogging(final int flags) {
278            // TODO: fix this?
279            // Protocol logging
280            Eas.setUserDebug(flags);
281            // Sync logging
282            //setUserDebug(flags);
283        }
284
285        @Override
286        public void loadAttachment(final long attachmentId, final boolean background) {
287            LogUtils.d(TAG, "IEmailService.loadAttachment");
288            // TODO: Implement.
289            /*
290            Attachment att = Attachment.restoreAttachmentWithId(ExchangeService.this, attachmentId);
291            log("loadAttachment " + attachmentId + ": " + att.mFileName);
292            sendMessageRequest(new PartRequest(att, null, null));
293            */
294        }
295
296        @Override
297        public void sendMeetingResponse(final long messageId, final int response) {
298            LogUtils.d(TAG, "IEmailService.sendMeetingResponse");
299            // TODO: Implement.
300            //sendMessageRequest(new MeetingResponseRequest(messageId, response));
301        }
302
303        /**
304         * Delete PIM (calendar, contacts) data for the specified account
305         *
306         * @param accountId the account whose data should be deleted
307         */
308        @Override
309        public void deleteAccountPIMData(final long accountId) {
310            LogUtils.d(TAG, "IEmailService.deleteAccountPIMData");
311            // TODO: Implement
312            /*
313            SyncManager exchangeService = INSTANCE;
314            if (exchangeService == null) return;
315            // Stop any running syncs
316            ExchangeService.stopAccountSyncs(accountId);
317            // Delete the data
318            ExchangeService.deleteAccountPIMData(ExchangeService.this, accountId);
319            long accountMailboxId = Mailbox.findMailboxOfType(exchangeService, accountId,
320                    Mailbox.TYPE_EAS_ACCOUNT_MAILBOX);
321            if (accountMailboxId != Mailbox.NO_MAILBOX) {
322                // Make sure the account mailbox is held due to security
323                synchronized(sSyncLock) {
324                    mSyncErrorMap.put(accountMailboxId, exchangeService.new SyncError(
325                            AbstractSyncService.EXIT_SECURITY_FAILURE, false));
326
327                }
328            }
329            // Make sure the reconciler runs
330            runAccountReconcilerSync(ExchangeService.this);
331            */
332        }
333
334        @Override
335        public int searchMessages(final long accountId, final SearchParams searchParams,
336                final long destMailboxId) {
337            LogUtils.d(TAG, "IEmailService.searchMessages");
338            return Search.searchMessages(EmailSyncAdapterService.this, accountId, searchParams,
339                    destMailboxId);
340
341        }
342
343        @Override
344        public void sendMail(final long accountId) {}
345
346        @Override
347        public int getCapabilities(final Account acct) {
348            String easVersion = acct.mProtocolVersion;
349            Double easVersionDouble = 2.5D;
350            if (easVersion != null) {
351                try {
352                    easVersionDouble = Double.parseDouble(easVersion);
353                } catch (NumberFormatException e) {
354                    // Stick with 2.5
355                }
356            }
357            if (easVersionDouble >= 12.0D) {
358                return AccountCapabilities.SYNCABLE_FOLDERS |
359                        AccountCapabilities.SERVER_SEARCH |
360                        AccountCapabilities.FOLDER_SERVER_SEARCH |
361                        AccountCapabilities.SANITIZED_HTML |
362                        AccountCapabilities.SMART_REPLY |
363                        AccountCapabilities.SERVER_SEARCH |
364                        AccountCapabilities.UNDO;
365            } else {
366                return AccountCapabilities.SYNCABLE_FOLDERS |
367                        AccountCapabilities.SANITIZED_HTML |
368                        AccountCapabilities.SMART_REPLY |
369                        AccountCapabilities.UNDO;
370            }
371        }
372
373        @Override
374        public void serviceUpdated(final String emailAddress) {
375            // Not required for EAS
376        }
377
378        // All IEmailService messages below are UNCALLED in Email.
379        // TODO: Remove.
380        @Deprecated
381        @Override
382        public int getApiLevel() {
383            return Api.LEVEL;
384        }
385
386        @Deprecated
387        @Override
388        public void startSync(long mailboxId, boolean userRequest, int deltaMessageCount) {}
389
390        @Deprecated
391        @Override
392        public void stopSync(long mailboxId) {}
393
394        @Deprecated
395        @Override
396        public void loadMore(long messageId) {}
397
398        @Deprecated
399        @Override
400        public boolean createFolder(long accountId, String name) {
401            return false;
402        }
403
404        @Deprecated
405        @Override
406        public boolean deleteFolder(long accountId, String name) {
407            return false;
408        }
409
410        @Deprecated
411        @Override
412        public boolean renameFolder(long accountId, String oldName, String newName) {
413            return false;
414        }
415
416        @Deprecated
417        @Override
418        public void hostChanged(long accountId) {}
419    };
420
421    public EmailSyncAdapterService() {
422        super();
423    }
424
425    @Override
426    public IBinder onBind(Intent intent) {
427        if (intent.getAction().equals(Eas.EXCHANGE_SERVICE_INTENT_ACTION)) {
428            return mBinder;
429        }
430        return super.onBind(intent);
431    }
432
433    @Override
434    protected AbstractThreadedSyncAdapter newSyncAdapter() {
435        return new SyncAdapterImpl(this);
436    }
437
438    // TODO: Handle cancelSync() appropriately.
439    private class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
440        public SyncAdapterImpl(Context context) {
441            super(context, true /* autoInitialize */);
442        }
443
444        @Override
445        public void onPerformSync(final android.accounts.Account acct, final Bundle extras,
446                final String authority, final ContentProviderClient provider,
447                final SyncResult syncResult) {
448            LogUtils.i(TAG, "performSync: extras = %s", extras.toString());
449            TempDirectory.setTempDirectory(EmailSyncAdapterService.this);
450
451            // TODO: Perform any connectivity checks, bail early if we don't have proper network
452            // for this sync operation.
453
454            final Context context = getContext();
455            final ContentResolver cr = context.getContentResolver();
456
457            // Get the EmailContent Account
458            final Account account;
459            final Cursor accountCursor = cr.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION,
460                    AccountColumns.EMAIL_ADDRESS + "=?", new String[] {acct.name}, null);
461            try {
462                if (!accountCursor.moveToFirst()) {
463                    // Could not load account.
464                    // TODO: improve error handling.
465                    return;
466                }
467                account = new Account();
468                account.restore(accountCursor);
469            } finally {
470                accountCursor.close();
471            }
472
473            // Do the bookkeeping for starting a sync, including stopping a ping if necessary.
474            mSyncHandlerMap.startSync(account.mId);
475
476            // TODO: Should we refresh the account here? It may have changed while waiting for any
477            // pings to stop. It may not matter since the things that may have been twiddled might
478            // not affect syncing.
479
480            // There are three possibilities for Mailbox.SYNC_EXTRA_MAILBOX_ID:
481            // 1) It's Mailbox.SYNC_EXTRA_MAILBOX_ID_ACCOUNT_ONLY. Sync only the account data.
482            // 2) It's not present. Perform a full account sync.
483            // 3) It's a mailbox id (non-negative value). Sync that mailbox only.
484            final long mailboxId = extras.getLong(Mailbox.SYNC_EXTRA_MAILBOX_ID, FULL_ACCOUNT_SYNC);
485            if (mailboxId == FULL_ACCOUNT_SYNC ||
486                    mailboxId == Mailbox.SYNC_EXTRA_MAILBOX_ID_ACCOUNT_ONLY) {
487                final EasAccountSyncHandler accountSyncHandler =
488                        new EasAccountSyncHandler(context, account);
489                accountSyncHandler.performSync();
490
491                if (mailboxId == Mailbox.NO_MAILBOX) {
492                    // Full account sync includes all mailboxes that participate in system sync.
493                    final Cursor c = Mailbox.getMailboxIdsForSync(cr, account.mId);
494                    if (c != null) {
495                        try {
496                            while (c.moveToNext()) {
497                                syncMailbox(context, cr, acct, account, c.getLong(0), extras,
498                                        syncResult);
499                            }
500                        } finally {
501                            c.close();
502                        }
503                    }
504                }
505            } else {
506                // Sync the mailbox that was explicitly requested.
507                if (!syncMailbox(context, cr, acct, account, mailboxId, extras, syncResult)) {
508                    // We can't sync this mailbox, so just send the expected UI callbacks.
509                    EmailServiceStatus.syncMailboxStatus(cr, extras, mailboxId,
510                            EmailServiceStatus.IN_PROGRESS, 0);
511                    EmailServiceStatus.syncMailboxStatus(cr, extras, mailboxId,
512                            EmailServiceStatus.SUCCESS, 0);
513                }
514            }
515
516            // Signal any waiting ping that it's good to go now.
517            mSyncHandlerMap.signalDone(account.mId, true, true);
518
519            // TODO: It may make sense to have common error handling here. Two possible mechanisms:
520            // 1) performSync return value can signal some useful info.
521            // 2) syncResult can contain useful info.
522        }
523
524        private boolean syncMailbox(final Context context, final ContentResolver cr,
525                final android.accounts.Account acct, final Account account, final long mailboxId,
526                final Bundle extras, final SyncResult syncResult) {
527            final Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
528            if (mailbox == null) {
529                return false;
530            }
531
532            if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
533                final EasOutboxSyncHandler outboxSyncHandler =
534                        new EasOutboxSyncHandler(context, account, mailbox);
535                outboxSyncHandler.performSync();
536            } else {
537                final EasSyncHandler syncHandler = EasSyncHandler.getEasSyncHandler(context, cr,
538                        acct, account, mailbox, extras, syncResult);
539                if (syncHandler == null) {
540                    return false;
541                }
542                syncHandler.performSync();
543            }
544            return true;
545        }
546    }
547}
548