• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.exchange.service;
18 
19 import android.app.AlarmManager;
20 import android.app.Notification;
21 import android.app.Notification.Builder;
22 import android.app.NotificationManager;
23 import android.app.PendingIntent;
24 import android.content.AbstractThreadedSyncAdapter;
25 import android.content.ComponentName;
26 import android.content.ContentProviderClient;
27 import android.content.ContentResolver;
28 import android.content.ContentValues;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.ServiceConnection;
32 import android.content.SyncResult;
33 import android.database.Cursor;
34 import android.net.Uri;
35 import android.os.AsyncTask;
36 import android.os.Bundle;
37 import android.os.IBinder;
38 import android.os.RemoteException;
39 import android.os.SystemClock;
40 import android.provider.CalendarContract;
41 import android.provider.ContactsContract;
42 import android.text.TextUtils;
43 import android.text.format.DateUtils;
44 import android.util.Log;
45 
46 import com.android.emailcommon.TempDirectory;
47 import com.android.emailcommon.provider.Account;
48 import com.android.emailcommon.provider.EmailContent;
49 import com.android.emailcommon.provider.EmailContent.AccountColumns;
50 import com.android.emailcommon.provider.EmailContent.Message;
51 import com.android.emailcommon.provider.EmailContent.MessageColumns;
52 import com.android.emailcommon.provider.EmailContent.SyncColumns;
53 import com.android.emailcommon.provider.HostAuth;
54 import com.android.emailcommon.provider.Mailbox;
55 import com.android.emailcommon.service.EmailServiceStatus;
56 import com.android.emailcommon.service.IEmailService;
57 import com.android.emailcommon.service.IEmailServiceCallback;
58 import com.android.emailcommon.service.SearchParams;
59 import com.android.emailcommon.service.ServiceProxy;
60 import com.android.emailcommon.utility.IntentUtilities;
61 import com.android.emailcommon.utility.Utility;
62 import com.android.exchange.Eas;
63 import com.android.exchange.R.drawable;
64 import com.android.exchange.R.string;
65 import com.android.exchange.adapter.PingParser;
66 import com.android.exchange.eas.EasSyncContacts;
67 import com.android.exchange.eas.EasSyncCalendar;
68 import com.android.exchange.eas.EasFolderSync;
69 import com.android.exchange.eas.EasLoadAttachment;
70 import com.android.exchange.eas.EasMoveItems;
71 import com.android.exchange.eas.EasOperation;
72 import com.android.exchange.eas.EasOutboxSync;
73 import com.android.exchange.eas.EasPing;
74 import com.android.exchange.eas.EasSearch;
75 import com.android.exchange.eas.EasSync;
76 import com.android.exchange.eas.EasSyncBase;
77 import com.android.mail.providers.UIProvider;
78 import com.android.mail.utils.LogUtils;
79 
80 import java.util.HashMap;
81 import java.util.HashSet;
82 
83 /**
84  * Service for communicating with Exchange servers. There are three main parts of this class:
85  * TODO: Flesh out these comments.
86  * 1) An {@link AbstractThreadedSyncAdapter} to handle actually performing syncs.
87  * 2) Bookkeeping for running Ping requests, which handles push notifications.
88  * 3) An {@link IEmailService} Stub to handle RPC from the UI.
89  */
90 public class EmailSyncAdapterService extends AbstractSyncAdapterService {
91 
92     private static final String TAG = Eas.LOG_TAG;
93 
94     /**
95      * Temporary while converting to EasService. Do not check in set to true.
96      * When true, delegates various operations to {@link EasService}, for use while developing the
97      * new service.
98      * The two following fields are used to support what happens when this is true.
99      */
100     private static final boolean DELEGATE_TO_EAS_SERVICE = false;
101     private IEmailService mEasService;
102     private ServiceConnection mConnection;
103 
104     private static final String EXTRA_START_PING = "START_PING";
105     private static final String EXTRA_PING_ACCOUNT = "PING_ACCOUNT";
106     private static final long SYNC_ERROR_BACKOFF_MILLIS = 5 * DateUtils.MINUTE_IN_MILLIS;
107 
108     /**
109      * The amount of time between periodic syncs intended to ensure that push hasn't died.
110      */
111     private static final long KICK_SYNC_INTERVAL =
112             DateUtils.HOUR_IN_MILLIS / DateUtils.SECOND_IN_MILLIS;
113 
114     /** Controls whether we do a periodic "kick" to restart the ping. */
115     private static final boolean SCHEDULE_KICK = true;
116 
117     /** Projection used for getting email address for an account. */
118     private static final String[] ACCOUNT_EMAIL_PROJECTION = { AccountColumns.EMAIL_ADDRESS };
119 
120     private static final Object sSyncAdapterLock = new Object();
121     private static AbstractThreadedSyncAdapter sSyncAdapter = null;
122 
123     // Value for a message's server id when sending fails.
124     public static final int SEND_FAILED = 1;
125     public static final String MAILBOX_KEY_AND_NOT_SEND_FAILED =
126             MessageColumns.MAILBOX_KEY + "=? and (" + SyncColumns.SERVER_ID + " is null or " +
127             SyncColumns.SERVER_ID + "!=" + SEND_FAILED + ')';
128 
129     /**
130      * Bookkeeping for handling synchronization between pings and syncs.
131      * "Ping" refers to a hanging POST or GET that is used to receive push notifications. Ping is
132      * the term for the Exchange command, but this code should be generic enough to be easily
133      * extended to IMAP.
134      * "Sync" refers to an actual sync command to either fetch mail state, account state, or send
135      * mail (send is implemented as "sync the outbox").
136      * TODO: Outbox sync probably need not stop a ping in progress.
137      * Basic rules of how these interact (note that all rules are per account):
138      * - Only one ping or sync may run at a time.
139      * - Due to how {@link AbstractThreadedSyncAdapter} works, sync requests will not occur while
140      *   a sync is in progress.
141      * - On the other hand, ping requests may come in while handling a ping.
142      * - "Ping request" is shorthand for "a request to change our ping parameters", which includes
143      *   a request to stop receiving push notifications.
144      * - If neither a ping nor a sync is running, then a request for either will run it.
145      * - If a sync is running, new ping requests block until the sync completes.
146      * - If a ping is running, a new sync request stops the ping and creates a pending ping
147      *   (which blocks until the sync completes).
148      * - If a ping is running, a new ping request stops the ping and either starts a new one or
149      *   does nothing, as appopriate (since a ping request can be to stop pushing).
150      * - As an optimization, while a ping request is waiting to run, subsequent ping requests are
151      *   ignored (the pending ping will pick up the latest ping parameters at the time it runs).
152      */
153     public class SyncHandlerSynchronizer {
154         /**
155          * Map of account id -> ping handler.
156          * For a given account id, there are three possible states:
157          * 1) If no ping or sync is currently running, there is no entry in the map for the account.
158          * 2) If a ping is running, there is an entry with the appropriate ping handler.
159          * 3) If there is a sync running, there is an entry with null as the value.
160          * We cannot have more than one ping or sync running at a time.
161          */
162         private final HashMap<Long, PingTask> mPingHandlers = new HashMap<Long, PingTask>();
163 
164         /**
165          * Wait until neither a sync nor a ping is running on this account, and then return.
166          * If there's a ping running, actively stop it. (For syncs, we have to just wait.)
167          * @param accountId The account we want to wait for.
168          */
waitUntilNoActivity(final long accountId)169         private synchronized void waitUntilNoActivity(final long accountId) {
170             while (mPingHandlers.containsKey(accountId)) {
171                 final PingTask pingHandler = mPingHandlers.get(accountId);
172                 if (pingHandler != null) {
173                     pingHandler.stop();
174                 }
175                 try {
176                     wait();
177                 } catch (final InterruptedException e) {
178                     // TODO: When would this happen, and how should I handle it?
179                 }
180             }
181         }
182 
183         /**
184          * Use this to see if we're currently syncing, as opposed to pinging or doing nothing.
185          * @param accountId The account to check.
186          * @return Whether that account is currently running a sync.
187          */
isRunningSync(final long accountId)188         private synchronized boolean isRunningSync(final long accountId) {
189             return (mPingHandlers.containsKey(accountId) && mPingHandlers.get(accountId) == null);
190         }
191 
192         /**
193          * If there are no running pings, stop the service.
194          */
stopServiceIfNoPings()195         private void stopServiceIfNoPings() {
196             for (final PingTask pingHandler : mPingHandlers.values()) {
197                 if (pingHandler != null) {
198                     return;
199                 }
200             }
201             EmailSyncAdapterService.this.stopSelf();
202         }
203 
204         /**
205          * Called prior to starting a sync to update our bookkeeping. We don't actually run the sync
206          * here; the caller must do that.
207          * @param accountId The account on which we are running a sync.
208          */
startSync(final long accountId)209         public synchronized void startSync(final long accountId) {
210             waitUntilNoActivity(accountId);
211             mPingHandlers.put(accountId, null);
212         }
213 
214         /**
215          * Starts or restarts a ping for an account, if the current account state indicates that it
216          * wants to push.
217          * @param account The account whose ping is being modified.
218          */
modifyPing(final boolean lastSyncHadError, final Account account)219         public synchronized void modifyPing(final boolean lastSyncHadError,
220                 final Account account) {
221             // If a sync is currently running, it will start a ping when it's done, so there's no
222             // need to do anything right now.
223             if (isRunningSync(account.mId)) {
224                 return;
225             }
226 
227             // Don't ping if we're on security hold.
228             if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
229                 return;
230             }
231 
232             // Don't ping for accounts that haven't performed initial sync.
233             if (EmailContent.isInitialSyncKey(account.mSyncKey)) {
234                 return;
235             }
236 
237             // Determine if this account needs pushes. All of the following must be true:
238             // - The account's sync interval must indicate that it wants push.
239             // - At least one content type must be sync-enabled in the account manager.
240             // - At least one mailbox of a sync-enabled type must have automatic sync enabled.
241             final EmailSyncAdapterService service = EmailSyncAdapterService.this;
242             final android.accounts.Account amAccount = new android.accounts.Account(
243                             account.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
244             boolean pushNeeded = false;
245             if (account.mSyncInterval == Account.CHECK_INTERVAL_PUSH) {
246                 final HashSet<String> authsToSync = getAuthsToSync(amAccount);
247                 // If we have at least one sync-enabled content type, check for syncing mailboxes.
248                 if (!authsToSync.isEmpty()) {
249                     final Cursor c = Mailbox.getMailboxesForPush(service.getContentResolver(),
250                             account.mId);
251                     if (c != null) {
252                         try {
253                             while (c.moveToNext()) {
254                                 final int mailboxType = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
255                                 if (authsToSync.contains(Mailbox.getAuthority(mailboxType))) {
256                                     pushNeeded = true;
257                                     break;
258                                 }
259                             }
260                         } finally {
261                             c.close();
262                         }
263                     }
264                 }
265             }
266 
267             // Stop, start, or restart the ping as needed, as well as the ping kicker periodic sync.
268             final PingTask pingSyncHandler = mPingHandlers.get(account.mId);
269             final Bundle extras = new Bundle(1);
270             extras.putBoolean(Mailbox.SYNC_EXTRA_PUSH_ONLY, true);
271             if (pushNeeded) {
272                 // First start or restart the ping as appropriate.
273                 if (pingSyncHandler != null) {
274                     pingSyncHandler.restart();
275                 } else {
276                     if (lastSyncHadError) {
277                         // Schedule an alarm to set up the ping in 5 minutes
278                         scheduleDelayedPing(amAccount, SYNC_ERROR_BACKOFF_MILLIS);
279                     } else {
280                         // Start a new ping.
281                         // Note: unlike startSync, we CANNOT allow the caller to do the actual work.
282                         // If we return before the ping starts, there's a race condition where
283                         // another ping or sync might start first. It only works for startSync
284                         // because sync is higher priority than ping (i.e. a ping can't start while
285                         // a sync is pending) and only one sync can run at a time.
286                         final PingTask pingHandler = new PingTask(service, account, amAccount,
287                                 this);
288                         mPingHandlers.put(account.mId, pingHandler);
289                         pingHandler.start();
290                         // Whenever we have a running ping, make sure this service stays running.
291                         service.startService(new Intent(service, EmailSyncAdapterService.class));
292                     }
293                 }
294                 if (SCHEDULE_KICK) {
295                     ContentResolver.addPeriodicSync(amAccount, EmailContent.AUTHORITY, extras,
296                                KICK_SYNC_INTERVAL);
297                 }
298             } else {
299                 if (pingSyncHandler != null) {
300                     pingSyncHandler.stop();
301                 }
302                 if (SCHEDULE_KICK) {
303                     ContentResolver.removePeriodicSync(amAccount, EmailContent.AUTHORITY, extras);
304                 }
305             }
306         }
307 
308         /**
309          * Updates the synchronization bookkeeping when a sync is done.
310          * @param account The account whose sync just finished.
311          */
syncComplete(final boolean lastSyncHadError, final Account account)312         public synchronized void syncComplete(final boolean lastSyncHadError,
313                 final Account account) {
314             LogUtils.d(TAG, "syncComplete, err: " + lastSyncHadError);
315             mPingHandlers.remove(account.mId);
316             // Syncs can interrupt pings, so we should check if we need to start one now.
317             // If the last sync had a fatal error, we will not immediately recreate the ping.
318             // Instead, we'll set an alarm that will restart them in a few minutes. This prevents
319             // a battery draining spin if there is some kind of protocol error or other
320             // non-transient failure. (Actually, immediately pinging even for a transient error
321             // isn't great)
322             modifyPing(lastSyncHadError, account);
323             stopServiceIfNoPings();
324             notifyAll();
325         }
326 
327         /**
328          * Updates the synchronization bookkeeping when a ping is done. Also requests a ping-only
329          * sync if necessary.
330          * @param amAccount The {@link android.accounts.Account} for this account.
331          * @param accountId The account whose ping just finished.
332          * @param pingStatus The status value from {@link PingParser} for the last ping performed.
333          *                   This cannot be one of the values that results in another ping, so this
334          *                   function only needs to handle the terminal statuses.
335          */
pingComplete(final android.accounts.Account amAccount, final long accountId, final int pingStatus)336         public synchronized void pingComplete(final android.accounts.Account amAccount,
337                 final long accountId, final int pingStatus) {
338             mPingHandlers.remove(accountId);
339 
340             // TODO: if (pingStatus == PingParser.STATUS_FAILED), notify UI.
341             // TODO: if (pingStatus == PingParser.STATUS_REQUEST_TOO_MANY_FOLDERS), notify UI.
342 
343             if (pingStatus == EasOperation.RESULT_REQUEST_FAILURE ||
344                     pingStatus == EasOperation.RESULT_OTHER_FAILURE) {
345                 // TODO: Sticky problem here: we necessarily aren't in a sync, so it's impossible to
346                 // signal the error to the SyncManager and take advantage of backoff there. Worse,
347                 // the current mechanism for how we do this will just encourage spammy requests
348                 // since the actual ping-only sync request ALWAYS succeeds.
349                 // So for now, let's delay a bit before asking the SyncManager to perform the sync.
350                 // Longer term, this should be incorporated into some form of backoff, either
351                 // by integrating with the SyncManager more fully or by implementing a Ping-specific
352                 // backoff mechanism (e.g. integrate this with the logic for ping duration).
353                 LogUtils.e(TAG, "Ping for account %d completed with error %d, delaying next ping",
354                         accountId, pingStatus);
355                 scheduleDelayedPing(amAccount, SYNC_ERROR_BACKOFF_MILLIS);
356             } else {
357                 stopServiceIfNoPings();
358             }
359 
360             // TODO: It might be the case that only STATUS_CHANGES_FOUND and
361             // STATUS_FOLDER_REFRESH_NEEDED need to notifyAll(). Think this through.
362             notifyAll();
363         }
364 
365     }
366     private final SyncHandlerSynchronizer mSyncHandlerMap = new SyncHandlerSynchronizer();
367 
368     /**
369      * The binder for IEmailService.
370      */
371     private final IEmailService.Stub mBinder = new IEmailService.Stub() {
372 
373         private String getEmailAddressForAccount(final long accountId) {
374             final String emailAddress = Utility.getFirstRowString(EmailSyncAdapterService.this,
375                     Account.CONTENT_URI, ACCOUNT_EMAIL_PROJECTION, Account.ID_SELECTION,
376                     new String[] {Long.toString(accountId)}, null, 0);
377             if (emailAddress == null) {
378                 LogUtils.e(TAG, "Could not find email address for account %d", accountId);
379             }
380             return emailAddress;
381         }
382 
383         @Override
384         public Bundle validate(final HostAuth hostAuth) {
385             LogUtils.d(TAG, "IEmailService.validate");
386             if (mEasService != null) {
387                 try {
388                     return mEasService.validate(hostAuth);
389                 } catch (final RemoteException re) {
390                     LogUtils.e(TAG, re, "While asking EasService to handle validate");
391                 }
392             }
393             return new EasFolderSync(EmailSyncAdapterService.this, hostAuth).doValidate();
394         }
395 
396         @Override
397         public Bundle autoDiscover(final String username, final String password) {
398             LogUtils.d(TAG, "IEmailService.autoDiscover");
399             return new EasAutoDiscover(EmailSyncAdapterService.this, username, password)
400                     .doAutodiscover();
401         }
402 
403         @Override
404         public void updateFolderList(final long accountId) {
405             LogUtils.d(TAG, "IEmailService.updateFolderList: %d", accountId);
406             if (mEasService != null) {
407                 try {
408                     mEasService.updateFolderList(accountId);
409                     return;
410                 } catch (final RemoteException re) {
411                     LogUtils.e(TAG, re, "While asking EasService to updateFolderList");
412                 }
413             }
414             final String emailAddress = getEmailAddressForAccount(accountId);
415             if (emailAddress != null) {
416                 final Bundle extras = new Bundle(1);
417                 extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
418                 ContentResolver.requestSync(new android.accounts.Account(
419                         emailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
420                         EmailContent.AUTHORITY, extras);
421             }
422         }
423 
424         @Override
425         public void setLogging(final int flags) {
426             // TODO: fix this?
427             // Protocol logging
428             Eas.setUserDebug(flags);
429             // Sync logging
430             //setUserDebug(flags);
431         }
432 
433         @Override
434         public void loadAttachment(final IEmailServiceCallback callback, final long accountId,
435                 final long attachmentId, final boolean background) {
436             LogUtils.d(TAG, "IEmailService.loadAttachment: %d", attachmentId);
437             // TODO: Prevent this from happening in parallel with a sync?
438             final EasLoadAttachment operation = new EasLoadAttachment(EmailSyncAdapterService.this,
439                     accountId, attachmentId, callback);
440             operation.performOperation();
441         }
442 
443         @Override
444         public void sendMeetingResponse(final long messageId, final int response) {
445             LogUtils.d(TAG, "IEmailService.sendMeetingResponse: %d, %d", messageId, response);
446             EasMeetingResponder.sendMeetingResponse(EmailSyncAdapterService.this, messageId,
447                     response);
448         }
449 
450         /**
451          * Delete PIM (calendar, contacts) data for the specified account
452          *
453          * @param emailAddress the email address for the account whose data should be deleted
454          */
455         @Override
456         public void deleteAccountPIMData(final String emailAddress) {
457             LogUtils.d(TAG, "IEmailService.deleteAccountPIMData");
458             if (emailAddress != null) {
459                 final Context context = EmailSyncAdapterService.this;
460                 EasSyncContacts.wipeAccountFromContentProvider(context, emailAddress);
461                 EasSyncCalendar.wipeAccountFromContentProvider(context, emailAddress);
462             }
463             // TODO: Run account reconciler?
464         }
465 
466         @Override
467         public int searchMessages(final long accountId, final SearchParams searchParams,
468                 final long destMailboxId) {
469             LogUtils.d(TAG, "IEmailService.searchMessages");
470             final EasSearch operation = new EasSearch(EmailSyncAdapterService.this, accountId,
471                     searchParams, destMailboxId);
472             operation.performOperation();
473             return operation.getTotalResults();
474             // TODO: may need an explicit callback to replace the one to IEmailServiceCallback.
475         }
476 
477         @Override
478         public void sendMail(final long accountId) {}
479 
480         @Override
481         public void pushModify(final long accountId) {
482             LogUtils.d(TAG, "IEmailService.pushModify");
483             if (mEasService != null) {
484                 try {
485                     mEasService.pushModify(accountId);
486                     return;
487                 } catch (final RemoteException re) {
488                     LogUtils.e(TAG, re, "While asking EasService to handle pushModify");
489                 }
490             }
491             final Account account = Account.restoreAccountWithId(EmailSyncAdapterService.this,
492                     accountId);
493             if (account != null) {
494                 mSyncHandlerMap.modifyPing(false, account);
495             }
496         }
497 
498         @Override
499         public void sync(final long accountId, final boolean updateFolderList,
500                 final int mailboxType, final long[] folders) {}
501     };
502 
EmailSyncAdapterService()503     public EmailSyncAdapterService() {
504         super();
505     }
506 
507     /**
508      * {@link AsyncTask} for restarting pings for all accounts that need it.
509      */
510     private static final String PUSH_ACCOUNTS_SELECTION =
511             AccountColumns.SYNC_INTERVAL + "=" + Integer.toString(Account.CHECK_INTERVAL_PUSH);
512     private class RestartPingsTask extends AsyncTask<Void, Void, Void> {
513 
514         private final ContentResolver mContentResolver;
515         private final SyncHandlerSynchronizer mSyncHandlerMap;
516         private boolean mAnyAccounts;
517 
RestartPingsTask(final ContentResolver contentResolver, final SyncHandlerSynchronizer syncHandlerMap)518         public RestartPingsTask(final ContentResolver contentResolver,
519                 final SyncHandlerSynchronizer syncHandlerMap) {
520             mContentResolver = contentResolver;
521             mSyncHandlerMap = syncHandlerMap;
522         }
523 
524         @Override
doInBackground(Void... params)525         protected Void doInBackground(Void... params) {
526             final Cursor c = mContentResolver.query(Account.CONTENT_URI,
527                     Account.CONTENT_PROJECTION, PUSH_ACCOUNTS_SELECTION, null, null);
528             if (c != null) {
529                 try {
530                     mAnyAccounts = (c.getCount() != 0);
531                     while (c.moveToNext()) {
532                         final Account account = new Account();
533                         account.restore(c);
534                         mSyncHandlerMap.modifyPing(false, account);
535                     }
536                 } finally {
537                     c.close();
538                 }
539             } else {
540                 mAnyAccounts = false;
541             }
542             return null;
543         }
544 
545         @Override
onPostExecute(Void result)546         protected void onPostExecute(Void result) {
547             if (!mAnyAccounts) {
548                 LogUtils.d(TAG, "stopping for no accounts");
549                 EmailSyncAdapterService.this.stopSelf();
550             }
551         }
552     }
553 
554     @Override
onCreate()555     public void onCreate() {
556         LogUtils.v(TAG, "onCreate()");
557         super.onCreate();
558         startService(new Intent(this, EmailSyncAdapterService.class));
559         // Restart push for all accounts that need it.
560         new RestartPingsTask(getContentResolver(), mSyncHandlerMap).executeOnExecutor(
561                 AsyncTask.THREAD_POOL_EXECUTOR);
562         if (DELEGATE_TO_EAS_SERVICE) {
563             // TODO: This block is temporary to support the transition to EasService.
564             mConnection = new ServiceConnection() {
565                 @Override
566                 public void onServiceConnected(ComponentName name,  IBinder binder) {
567                     mEasService = IEmailService.Stub.asInterface(binder);
568                 }
569 
570                 @Override
571                 public void onServiceDisconnected(ComponentName name) {
572                     mEasService = null;
573                 }
574             };
575             bindService(new Intent(this, EasService.class), mConnection, Context.BIND_AUTO_CREATE);
576         }
577     }
578 
579     @Override
onDestroy()580     public void onDestroy() {
581         LogUtils.v(TAG, "onDestroy()");
582         super.onDestroy();
583         for (PingTask task : mSyncHandlerMap.mPingHandlers.values()) {
584             if (task != null) {
585                 task.stop();
586             }
587         }
588         if (DELEGATE_TO_EAS_SERVICE) {
589             // TODO: This block is temporary to support the transition to EasService.
590             unbindService(mConnection);
591         }
592     }
593 
594     @Override
onBind(Intent intent)595     public IBinder onBind(Intent intent) {
596         if (intent.getAction().equals(Eas.EXCHANGE_SERVICE_INTENT_ACTION)) {
597             return mBinder;
598         }
599         return super.onBind(intent);
600     }
601 
602     @Override
onStartCommand(Intent intent, int flags, int startId)603     public int onStartCommand(Intent intent, int flags, int startId) {
604         if (intent != null &&
605                 TextUtils.equals(Eas.EXCHANGE_SERVICE_INTENT_ACTION, intent.getAction())) {
606             if (intent.getBooleanExtra(ServiceProxy.EXTRA_FORCE_SHUTDOWN, false)) {
607                 // We've been asked to forcibly shutdown. This happens if email accounts are
608                 // deleted, otherwise we can get errors if services are still running for
609                 // accounts that are now gone.
610                 // TODO: This is kind of a hack, it would be nicer if we could handle it correctly
611                 // if accounts disappear out from under us.
612                 LogUtils.d(TAG, "Forced shutdown, killing process");
613                 System.exit(-1);
614             } else if (intent.getBooleanExtra(EXTRA_START_PING, false)) {
615                 LogUtils.d(TAG, "Restarting ping from alarm");
616                 // We've been woken up by an alarm to restart our ping. This happens if a sync
617                 // fails, rather that instantly starting the ping, we'll hold off for a few minutes.
618                 final android.accounts.Account account =
619                         intent.getParcelableExtra(EXTRA_PING_ACCOUNT);
620                 EasPing.requestPing(account);
621             }
622         }
623         return super.onStartCommand(intent, flags, startId);
624     }
625 
626     @Override
getSyncAdapter()627     protected AbstractThreadedSyncAdapter getSyncAdapter() {
628         synchronized (sSyncAdapterLock) {
629             if (sSyncAdapter == null) {
630                 sSyncAdapter = new SyncAdapterImpl(this);
631             }
632             return sSyncAdapter;
633         }
634     }
635 
636     // TODO: Handle cancelSync() appropriately.
637     private class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
SyncAdapterImpl(Context context)638         public SyncAdapterImpl(Context context) {
639             super(context, true /* autoInitialize */);
640         }
641 
642         @Override
onPerformSync(final android.accounts.Account acct, final Bundle extras, final String authority, final ContentProviderClient provider, final SyncResult syncResult)643         public void onPerformSync(final android.accounts.Account acct, final Bundle extras,
644                 final String authority, final ContentProviderClient provider,
645                 final SyncResult syncResult) {
646             if (LogUtils.isLoggable(TAG, Log.DEBUG)) {
647                 LogUtils.d(TAG, "onPerformSync: %s, %s", acct.toString(), extras.toString());
648             } else {
649                 LogUtils.i(TAG, "onPerformSync: %s", extras.toString());
650             }
651             TempDirectory.setTempDirectory(EmailSyncAdapterService.this);
652 
653             // TODO: Perform any connectivity checks, bail early if we don't have proper network
654             // for this sync operation.
655 
656             final Context context = getContext();
657             final ContentResolver cr = context.getContentResolver();
658 
659             // Get the EmailContent Account
660             final Account account;
661             final Cursor accountCursor = cr.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION,
662                     AccountColumns.EMAIL_ADDRESS + "=?", new String[] {acct.name}, null);
663             try {
664                 if (!accountCursor.moveToFirst()) {
665                     // Could not load account.
666                     // TODO: improve error handling.
667                     LogUtils.w(TAG, "onPerformSync: could not load account");
668                     return;
669                 }
670                 account = new Account();
671                 account.restore(accountCursor);
672             } finally {
673                 accountCursor.close();
674             }
675 
676             // Figure out what we want to sync, based on the extras and our account sync status.
677             final boolean isInitialSync = EmailContent.isInitialSyncKey(account.mSyncKey);
678             final long[] mailboxIds = Mailbox.getMailboxIdsFromBundle(extras);
679             final int mailboxType = extras.getInt(Mailbox.SYNC_EXTRA_MAILBOX_TYPE,
680                     Mailbox.TYPE_NONE);
681 
682             // Push only means this sync request should only refresh the ping (either because
683             // settings changed, or we need to restart it for some reason).
684             final boolean pushOnly = Mailbox.isPushOnlyExtras(extras);
685             // Account only means just do a FolderSync.
686             final boolean accountOnly = Mailbox.isAccountOnlyExtras(extras);
687 
688             // A "full sync" means that we didn't request a more specific type of sync.
689             final boolean isFullSync = (!pushOnly && !accountOnly && mailboxIds == null &&
690                     mailboxType == Mailbox.TYPE_NONE);
691 
692             // A FolderSync is necessary for full sync, initial sync, and account only sync.
693             final boolean isFolderSync = (isFullSync || isInitialSync || accountOnly);
694 
695             // If we're just twiddling the push, we do the lightweight thing and bail early.
696             if (pushOnly && !isFolderSync) {
697                 LogUtils.d(TAG, "onPerformSync: mailbox push only");
698                 if (mEasService != null) {
699                     try {
700                         mEasService.pushModify(account.mId);
701                         return;
702                     } catch (final RemoteException re) {
703                         LogUtils.e(TAG, re, "While trying to pushModify within onPerformSync");
704                     }
705                 }
706                 mSyncHandlerMap.modifyPing(false, account);
707                 return;
708             }
709 
710             // Do the bookkeeping for starting a sync, including stopping a ping if necessary.
711             mSyncHandlerMap.startSync(account.mId);
712             int operationResult = 0;
713             try {
714                 // Perform a FolderSync if necessary.
715                 // TODO: We permit FolderSync even during security hold, because it's necessary to
716                 // resolve some holds. Ideally we would only do it for the holds that require it.
717                 if (isFolderSync) {
718                     final EasFolderSync folderSync = new EasFolderSync(context, account);
719                     operationResult = folderSync.doFolderSync();
720                     if (operationResult < 0) {
721                         return;
722                     }
723                 }
724 
725                 // Do not permit further syncs if we're on security hold.
726                 if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
727                     return;
728                 }
729 
730                 // Perform email upsync for this account. Moves first, then state changes.
731                 if (!isInitialSync) {
732                     EasMoveItems move = new EasMoveItems(context, account);
733                     operationResult = move.upsyncMovedMessages();
734                     if (operationResult < 0) {
735                         return;
736                     }
737 
738                     // TODO: EasSync should eventually handle both up and down; for now, it's used
739                     // purely for upsync.
740                     EasSync upsync = new EasSync(context, account);
741                     operationResult = upsync.upsync();
742                     if (operationResult < 0) {
743                         return;
744                     }
745                 }
746 
747                 if (mailboxIds != null) {
748                     final boolean hasCallbackMethod =
749                             extras.containsKey(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD);
750                     // Sync the mailbox that was explicitly requested.
751                     for (final long mailboxId : mailboxIds) {
752                         if (hasCallbackMethod) {
753                             EmailServiceStatus.syncMailboxStatus(cr, extras, mailboxId,
754                                     EmailServiceStatus.IN_PROGRESS, 0,
755                                     UIProvider.LastSyncResult.SUCCESS);
756                         }
757                         operationResult = syncMailbox(context, cr, acct, account, mailboxId,
758                                 extras, syncResult, null, true);
759                         if (hasCallbackMethod) {
760                             EmailServiceStatus.syncMailboxStatus(cr, extras,
761                                     mailboxId,EmailServiceStatus.SUCCESS, 0,
762                                     EasOperation.translateSyncResultToUiResult(operationResult));
763                         }
764 
765                         if (operationResult < 0) {
766                             break;
767                         }
768                     }
769                 } else if (!accountOnly && !pushOnly) {
770                     // We have to sync multiple folders.
771                     final Cursor c;
772                     if (isFullSync) {
773                         // Full account sync includes all mailboxes that participate in system sync.
774                         c = Mailbox.getMailboxIdsForSync(cr, account.mId);
775                     } else {
776                         // Type-filtered sync should only get the mailboxes of a specific type.
777                         c = Mailbox.getMailboxIdsForSyncByType(cr, account.mId, mailboxType);
778                     }
779                     if (c != null) {
780                         try {
781                             final HashSet<String> authsToSync = getAuthsToSync(acct);
782                             while (c.moveToNext()) {
783                                 operationResult = syncMailbox(context, cr, acct, account,
784                                         c.getLong(0), extras, syncResult, authsToSync, false);
785                                 if (operationResult < 0) {
786                                     break;
787                                 }
788                             }
789                         } finally {
790                             c.close();
791                         }
792                     }
793                 }
794             } finally {
795                 // Clean up the bookkeeping, including restarting ping if necessary.
796                 mSyncHandlerMap.syncComplete(syncResult.hasError(), account);
797 
798                 if (operationResult < 0) {
799                     EasFolderSync.writeResultToSyncResult(operationResult, syncResult);
800                     // If any operations had an auth error, notify the user.
801                     // Note that provisioning errors should have already triggered the policy
802                     // notification, so suppress those from showing the auth notification.
803                     if (syncResult.stats.numAuthExceptions > 0 &&
804                             operationResult != EasOperation.RESULT_PROVISIONING_ERROR) {
805                         showAuthNotification(account.mId, account.mEmailAddress);
806                     }
807                 }
808 
809                 LogUtils.d(TAG, "onPerformSync: finished");
810             }
811         }
812 
813         /**
814          * Update the mailbox's sync status with the provider and, if we're finished with the sync,
815          * write the last sync time as well.
816          * @param context Our {@link Context}.
817          * @param mailbox The mailbox whose sync status to update.
818          * @param cv A {@link ContentValues} object to use for updating the provider.
819          * @param syncStatus The status for the current sync.
820          */
updateMailbox(final Context context, final Mailbox mailbox, final ContentValues cv, final int syncStatus)821         private void updateMailbox(final Context context, final Mailbox mailbox,
822                 final ContentValues cv, final int syncStatus) {
823             cv.put(Mailbox.UI_SYNC_STATUS, syncStatus);
824             if (syncStatus == EmailContent.SYNC_STATUS_NONE) {
825                 cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
826             }
827             mailbox.update(context, cv);
828         }
829 
syncMailbox(final Context context, final ContentResolver cr, final android.accounts.Account acct, final Account account, final long mailboxId, final Bundle extras, final SyncResult syncResult, final HashSet<String> authsToSync, final boolean isMailboxSync)830         private int syncMailbox(final Context context, final ContentResolver cr,
831                 final android.accounts.Account acct, final Account account, final long mailboxId,
832                 final Bundle extras, final SyncResult syncResult, final HashSet<String> authsToSync,
833                 final boolean isMailboxSync) {
834             final Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
835             if (mailbox == null) {
836                 return EasSyncBase.RESULT_HARD_DATA_FAILURE;
837             }
838 
839             if (mailbox.mAccountKey != account.mId) {
840                 LogUtils.e(TAG, "Mailbox does not match account: %s, %s", acct.toString(),
841                         extras.toString());
842                 return EasSyncBase.RESULT_HARD_DATA_FAILURE;
843             }
844             if (authsToSync != null && !authsToSync.contains(Mailbox.getAuthority(mailbox.mType))) {
845                 // We are asking for an account sync, but this mailbox type is not configured for
846                 // sync. Do NOT treat this as a sync error for ping backoff purposes.
847                 return EasSyncBase.RESULT_DONE;
848             }
849 
850             if (mailbox.mType == Mailbox.TYPE_DRAFTS) {
851                 // TODO: Because we don't have bidirectional sync working, trying to downsync
852                 // the drafts folder is confusing. b/11158759
853                 // For now, just disable all syncing of DRAFTS type folders.
854                 // Automatic syncing should always be disabled, but we also stop it here to ensure
855                 // that we won't sync even if the user attempts to force a sync from the UI.
856                 // Do NOT treat as a sync error for ping backoff purposes.
857                 LogUtils.d(TAG, "Skipping sync of DRAFTS folder");
858                 return EasSyncBase.RESULT_DONE;
859             }
860 
861             // Non-mailbox syncs are whole account syncs initiated by the AccountManager and are
862             // treated as background syncs.
863             // TODO: Push will be treated as "user" syncs, and probably should be background.
864             if (mailbox.mType == Mailbox.TYPE_OUTBOX || mailbox.isSyncable()) {
865                 final ContentValues cv = new ContentValues(2);
866                 updateMailbox(context, mailbox, cv, isMailboxSync ?
867                         EmailContent.SYNC_STATUS_USER : EmailContent.SYNC_STATUS_BACKGROUND);
868                 try {
869                     if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
870                         return syncOutbox(context, cr, account, mailbox);
871                     }
872                     final EasSyncBase operation = new EasSyncBase(context, account, mailbox);
873                     return operation.performOperation();
874                 } finally {
875                     updateMailbox(context, mailbox, cv, EmailContent.SYNC_STATUS_NONE);
876                 }
877             }
878 
879             return EasSyncBase.RESULT_DONE;
880         }
881     }
882 
syncOutbox(Context context, ContentResolver cr, Account account, Mailbox mailbox)883     private int syncOutbox(Context context, ContentResolver cr, Account account, Mailbox mailbox) {
884         // Get a cursor to Outbox messages
885         final Cursor c = cr.query(Message.CONTENT_URI,
886                 Message.CONTENT_PROJECTION, MAILBOX_KEY_AND_NOT_SEND_FAILED,
887                 new String[] {Long.toString(mailbox.mId)}, null);
888         try {
889             // Loop through the messages, sending each one
890             while (c.moveToNext()) {
891                 final Message message = new Message();
892                 message.restore(c);
893                 if (Utility.hasUnloadedAttachments(context, message.mId)) {
894                     // We'll just have to wait on this...
895                     continue;
896                 }
897 
898                 // TODO: Fix -- how do we want to signal to UI that we started syncing?
899                 // Note the entire callback mechanism here needs improving.
900                 //sendMessageStatus(message.mId, null, EmailServiceStatus.IN_PROGRESS, 0);
901 
902                 EasOperation op = new EasOutboxSync(context, account, message, true);
903                 int result = op.performOperation();
904                 if (result == EasOutboxSync.RESULT_ITEM_NOT_FOUND) {
905                     // This can happen if we are using smartReply, and the message we are referring
906                     // to has disappeared from the server. Try again with smartReply disabled.
907                     op = new EasOutboxSync(context, account, message, false);
908                     result = op.performOperation();
909                 }
910                 // If we got some connection error or other fatal error, terminate the sync.
911                 if (result != EasOutboxSync.RESULT_OK &&
912                     result != EasOutboxSync.RESULT_NON_FATAL_ERROR &&
913                     result > EasOutboxSync.RESULT_OP_SPECIFIC_ERROR_RESULT) {
914                     LogUtils.w(TAG, "Aborting outbox sync for error %d", result);
915                     return result;
916                 }
917             }
918         } finally {
919             // TODO: Some sort of sendMessageStatus() is needed here.
920             c.close();
921         }
922         return EasOutboxSync.RESULT_OK;
923     }
924 
showAuthNotification(long accountId, String accountName)925     private void showAuthNotification(long accountId, String accountName) {
926         final PendingIntent pendingIntent = PendingIntent.getActivity(
927                 this,
928                 0,
929                 createAccountSettingsIntent(accountId, accountName),
930                 0);
931 
932         final Notification notification = new Builder(this)
933                 .setContentTitle(this.getString(string.auth_error_notification_title))
934                 .setContentText(this.getString(
935                         string.auth_error_notification_text, accountName))
936                 .setSmallIcon(drawable.stat_notify_auth)
937                 .setContentIntent(pendingIntent)
938                 .setAutoCancel(true)
939                 .build();
940 
941         final NotificationManager nm = (NotificationManager)
942                 this.getSystemService(Context.NOTIFICATION_SERVICE);
943         nm.notify("AuthError", 0, notification);
944     }
945 
946     /**
947      * Create and return an intent to display (and edit) settings for a specific account, or -1
948      * for any/all accounts.  If an account name string is provided, a warning dialog will be
949      * displayed as well.
950      */
createAccountSettingsIntent(long accountId, String accountName)951     public static Intent createAccountSettingsIntent(long accountId, String accountName) {
952         final Uri.Builder builder = IntentUtilities.createActivityIntentUrlBuilder(
953                 IntentUtilities.PATH_SETTINGS);
954         IntentUtilities.setAccountId(builder, accountId);
955         IntentUtilities.setAccountName(builder, accountName);
956         return new Intent(Intent.ACTION_EDIT, builder.build());
957     }
958 
959     /**
960      * Determine which content types are set to sync for an account.
961      * @param account The account whose sync settings we're looking for.
962      * @return The authorities for the content types we want to sync for account.
963      */
getAuthsToSync(final android.accounts.Account account)964     private static HashSet<String> getAuthsToSync(final android.accounts.Account account) {
965         final HashSet<String> authsToSync = new HashSet();
966         if (ContentResolver.getSyncAutomatically(account, EmailContent.AUTHORITY)) {
967             authsToSync.add(EmailContent.AUTHORITY);
968         }
969         if (ContentResolver.getSyncAutomatically(account, CalendarContract.AUTHORITY)) {
970             authsToSync.add(CalendarContract.AUTHORITY);
971         }
972         if (ContentResolver.getSyncAutomatically(account, ContactsContract.AUTHORITY)) {
973             authsToSync.add(ContactsContract.AUTHORITY);
974         }
975         return authsToSync;
976     }
977 
978     /**
979      * Schedule to have a ping start some time in the future. This is used when we encounter an
980      * error, and properly should be a more full featured back-off, but for the short run, just
981      * waiting a few minutes at least avoids burning battery.
982      * @param amAccount The account that needs to be pinged.
983      * @param delay The time in milliseconds to wait before requesting the ping-only sync. Note that
984      *              it may take longer than this before the ping actually happens, since there's two
985      *              layers of waiting ({@link AlarmManager} can choose to wait longer, as can the
986      *              SyncManager).
987      */
scheduleDelayedPing(final android.accounts.Account amAccount, final long delay)988     private void scheduleDelayedPing(final android.accounts.Account amAccount, final long delay) {
989         final Intent intent = new Intent(this, EmailSyncAdapterService.class);
990         intent.setAction(Eas.EXCHANGE_SERVICE_INTENT_ACTION);
991         intent.putExtra(EXTRA_START_PING, true);
992         intent.putExtra(EXTRA_PING_ACCOUNT, amAccount);
993         final PendingIntent pi = PendingIntent.getService(this, 0, intent,
994                 PendingIntent.FLAG_ONE_SHOT);
995         final AlarmManager am = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
996         final long atTime = SystemClock.elapsedRealtime() + delay;
997         am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, atTime, pi);
998     }
999 }
1000