• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008-2009 Marc Blank
3  * Licensed to The Android Open Source Project.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.exchange;
19 
20 import android.app.AlarmManager;
21 import android.app.PendingIntent;
22 import android.app.Service;
23 import android.content.BroadcastReceiver;
24 import android.content.ContentResolver;
25 import android.content.ContentUris;
26 import android.content.ContentValues;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.IntentFilter;
30 import android.database.ContentObserver;
31 import android.database.Cursor;
32 import android.net.ConnectivityManager;
33 import android.net.NetworkInfo;
34 import android.net.NetworkInfo.State;
35 import android.net.Uri;
36 import android.os.Bundle;
37 import android.os.Debug;
38 import android.os.Handler;
39 import android.os.IBinder;
40 import android.os.PowerManager;
41 import android.os.PowerManager.WakeLock;
42 import android.os.Process;
43 import android.os.RemoteCallbackList;
44 import android.os.RemoteException;
45 import android.provider.CalendarContract;
46 import android.provider.CalendarContract.Calendars;
47 import android.provider.CalendarContract.Events;
48 import android.provider.ContactsContract;
49 import android.util.Log;
50 
51 import com.android.emailcommon.Api;
52 import com.android.emailcommon.TempDirectory;
53 import com.android.emailcommon.provider.Account;
54 import com.android.emailcommon.provider.EmailContent;
55 import com.android.emailcommon.provider.EmailContent.Attachment;
56 import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
57 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
58 import com.android.emailcommon.provider.EmailContent.Message;
59 import com.android.emailcommon.provider.EmailContent.SyncColumns;
60 import com.android.emailcommon.provider.HostAuth;
61 import com.android.emailcommon.provider.Mailbox;
62 import com.android.emailcommon.provider.Policy;
63 import com.android.emailcommon.provider.ProviderUnavailableException;
64 import com.android.emailcommon.service.AccountServiceProxy;
65 import com.android.emailcommon.service.EmailServiceProxy;
66 import com.android.emailcommon.service.EmailServiceStatus;
67 import com.android.emailcommon.service.IEmailService;
68 import com.android.emailcommon.service.IEmailServiceCallback;
69 import com.android.emailcommon.service.PolicyServiceProxy;
70 import com.android.emailcommon.service.SearchParams;
71 import com.android.emailcommon.utility.EmailAsyncTask;
72 import com.android.emailcommon.utility.EmailClientConnectionManager;
73 import com.android.emailcommon.utility.Utility;
74 import com.android.exchange.adapter.CalendarSyncAdapter;
75 import com.android.exchange.adapter.ContactsSyncAdapter;
76 import com.android.exchange.adapter.Search;
77 import com.android.exchange.provider.MailboxUtilities;
78 import com.android.exchange.utility.FileLogger;
79 
80 import org.apache.http.conn.params.ConnManagerPNames;
81 import org.apache.http.conn.params.ConnPerRoute;
82 import org.apache.http.conn.routing.HttpRoute;
83 import org.apache.http.params.BasicHttpParams;
84 import org.apache.http.params.HttpParams;
85 
86 import java.io.IOException;
87 import java.util.ArrayList;
88 import java.util.HashMap;
89 import java.util.List;
90 import java.util.concurrent.ConcurrentHashMap;
91 
92 /**
93  * The ExchangeService handles all aspects of starting, maintaining, and stopping the various sync
94  * adapters used by Exchange.  However, it is capable of handing any kind of email sync, and it
95  * would be appropriate to use for IMAP push, when that functionality is added to the Email
96  * application.
97  *
98  * The Email application communicates with EAS sync adapters via ExchangeService's binder interface,
99  * which exposes UI-related functionality to the application (see the definitions below)
100  *
101  * ExchangeService uses ContentObservers to detect changes to accounts, mailboxes, and messages in
102  * order to maintain proper 2-way syncing of data.  (More documentation to follow)
103  *
104  */
105 public class ExchangeService extends Service implements Runnable {
106 
107     private static final String TAG = "ExchangeService";
108 
109     // The ExchangeService's mailbox "id"
110     public static final int EXTRA_MAILBOX_ID = -1;
111     public static final int EXCHANGE_SERVICE_MAILBOX_ID = 0;
112 
113     private static final int SECONDS = 1000;
114     private static final int MINUTES = 60*SECONDS;
115     private static final int ONE_DAY_MINUTES = 1440;
116 
117     private static final int EXCHANGE_SERVICE_HEARTBEAT_TIME = 15*MINUTES;
118     private static final int CONNECTIVITY_WAIT_TIME = 10*MINUTES;
119 
120     // Sync hold constants for services with transient errors
121     private static final int HOLD_DELAY_MAXIMUM = 4*MINUTES;
122 
123     // Reason codes when ExchangeService.kick is called (mainly for debugging)
124     // UI has changed data, requiring an upsync of changes
125     public static final int SYNC_UPSYNC = 0;
126     // A scheduled sync (when not using push)
127     public static final int SYNC_SCHEDULED = 1;
128     // Mailbox was marked push
129     public static final int SYNC_PUSH = 2;
130     // A ping (EAS push signal) was received
131     public static final int SYNC_PING = 3;
132     // Misc.
133     public static final int SYNC_KICK = 4;
134     // A part request (attachment load, for now) was sent to ExchangeService
135     public static final int SYNC_SERVICE_PART_REQUEST = 5;
136 
137     // Requests >= SYNC_CALLBACK_START generate callbacks to the UI
138     public static final int SYNC_CALLBACK_START = 6;
139     // startSync was requested of ExchangeService (other than due to user request)
140     public static final int SYNC_SERVICE_START_SYNC = SYNC_CALLBACK_START + 0;
141     // startSync was requested of ExchangeService (due to user request)
142     public static final int SYNC_UI_REQUEST = SYNC_CALLBACK_START + 1;
143 
144     private static final String WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX =
145         MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.TYPE + "!=" +
146         Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + " and " + MailboxColumns.SYNC_INTERVAL +
147         " IN (" + Mailbox.CHECK_INTERVAL_PING + ',' + Mailbox.CHECK_INTERVAL_PUSH + ')';
148     protected static final String WHERE_IN_ACCOUNT_AND_PUSHABLE =
149         MailboxColumns.ACCOUNT_KEY + "=? and type in (" + Mailbox.TYPE_INBOX + ','
150         + Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + ',' + Mailbox.TYPE_CONTACTS + ','
151         + Mailbox.TYPE_CALENDAR + ')';
152     protected static final String WHERE_IN_ACCOUNT_AND_TYPE_INBOX =
153         MailboxColumns.ACCOUNT_KEY + "=? and type = " + Mailbox.TYPE_INBOX ;
154     private static final String WHERE_MAILBOX_KEY = Message.MAILBOX_KEY + "=?";
155     private static final String WHERE_PROTOCOL_EAS = HostAuthColumns.PROTOCOL + "=\"" +
156         AbstractSyncService.EAS_PROTOCOL + "\"";
157     private static final String WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN =
158         "(" + MailboxColumns.TYPE + '=' + Mailbox.TYPE_OUTBOX
159         + " or " + MailboxColumns.SYNC_INTERVAL + "!=" + Mailbox.CHECK_INTERVAL_NEVER + ')'
160         + " and " + MailboxColumns.ACCOUNT_KEY + " in (";
161     private static final String ACCOUNT_KEY_IN = MailboxColumns.ACCOUNT_KEY + " in (";
162     private static final String WHERE_CALENDAR_ID = Events.CALENDAR_ID + "=?";
163 
164     // Offsets into the syncStatus data for EAS that indicate type, exit status, and change count
165     // The format is S<type_char>:<exit_char>:<change_count>
166     public static final int STATUS_TYPE_CHAR = 1;
167     public static final int STATUS_EXIT_CHAR = 3;
168     public static final int STATUS_CHANGE_COUNT_OFFSET = 5;
169 
170     // Ready for ping
171     public static final int PING_STATUS_OK = 0;
172     // Service already running (can't ping)
173     public static final int PING_STATUS_RUNNING = 1;
174     // Service waiting after I/O error (can't ping)
175     public static final int PING_STATUS_WAITING = 2;
176     // Service had a fatal error; can't run
177     public static final int PING_STATUS_UNABLE = 3;
178 
179     private static final int MAX_CLIENT_CONNECTION_MANAGER_SHUTDOWNS = 1;
180 
181     // We synchronize on this for all actions affecting the service and error maps
182     private static final Object sSyncLock = new Object();
183     // All threads can use this lock to wait for connectivity
184     public static final Object sConnectivityLock = new Object();
185     public static boolean sConnectivityHold = false;
186 
187     // Keeps track of running services (by mailbox id)
188     private final HashMap<Long, AbstractSyncService> mServiceMap =
189         new HashMap<Long, AbstractSyncService>();
190     // Keeps track of services whose last sync ended with an error (by mailbox id)
191     /*package*/ ConcurrentHashMap<Long, SyncError> mSyncErrorMap =
192         new ConcurrentHashMap<Long, SyncError>();
193     // Keeps track of which services require a wake lock (by mailbox id)
194     private final HashMap<Long, Boolean> mWakeLocks = new HashMap<Long, Boolean>();
195     // Keeps track of PendingIntents for mailbox alarms (by mailbox id)
196     private final HashMap<Long, PendingIntent> mPendingIntents = new HashMap<Long, PendingIntent>();
197     // The actual WakeLock obtained by ExchangeService
198     private WakeLock mWakeLock = null;
199     // Keep our cached list of active Accounts here
200     public final AccountList mAccountList = new AccountList();
201 
202     // Observers that we use to look for changed mail-related data
203     private final Handler mHandler = new Handler();
204     private AccountObserver mAccountObserver;
205     private MailboxObserver mMailboxObserver;
206     private SyncedMessageObserver mSyncedMessageObserver;
207 
208     // Concurrent because CalendarSyncAdapter can modify the map during a wipe
209     private final ConcurrentHashMap<Long, CalendarObserver> mCalendarObservers =
210         new ConcurrentHashMap<Long, CalendarObserver>();
211 
212     private ContentResolver mResolver;
213 
214     // The singleton ExchangeService object, with its thread and stop flag
215     protected static ExchangeService INSTANCE;
216     private static Thread sServiceThread = null;
217     // Cached unique device id
218     private static String sDeviceId = null;
219     // ConnectionManager that all EAS threads can use
220     private static EmailClientConnectionManager sClientConnectionManager = null;
221     // Count of ClientConnectionManager shutdowns
222     private static volatile int sClientConnectionManagerShutdownCount = 0;
223 
224     private static volatile boolean sStartingUp = false;
225     private static volatile boolean sStop = false;
226 
227     // The reason for ExchangeService's next wakeup call
228     private String mNextWaitReason;
229     // Whether we have an unsatisfied "kick" pending
230     private boolean mKicked = false;
231 
232     // Receiver of connectivity broadcasts
233     private ConnectivityReceiver mConnectivityReceiver = null;
234     private ConnectivityReceiver mBackgroundDataSettingReceiver = null;
235     private volatile boolean mBackgroundData = true;
236     // The most current NetworkInfo (from ConnectivityManager)
237     private NetworkInfo mNetworkInfo;
238 
239     // Callbacks as set up via setCallback
240     private final RemoteCallbackList<IEmailServiceCallback> mCallbackList =
241         new RemoteCallbackList<IEmailServiceCallback>();
242 
243     private interface ServiceCallbackWrapper {
call(IEmailServiceCallback cb)244         public void call(IEmailServiceCallback cb) throws RemoteException;
245     }
246 
247     /**
248      * Proxy that can be used by various sync adapters to tie into ExchangeService's callback system
249      * Used this way:  ExchangeService.callback().callbackMethod(args...);
250      * The proxy wraps checking for existence of a ExchangeService instance
251      * Failures of these callbacks can be safely ignored.
252      */
253     static private final IEmailServiceCallback.Stub sCallbackProxy =
254         new IEmailServiceCallback.Stub() {
255 
256         /**
257          * Broadcast a callback to the everyone that's registered
258          *
259          * @param wrapper the ServiceCallbackWrapper used in the broadcast
260          */
261         private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) {
262             RemoteCallbackList<IEmailServiceCallback> callbackList =
263                 (INSTANCE == null) ? null: INSTANCE.mCallbackList;
264             if (callbackList != null) {
265                 // Call everyone on our callback list
266                 int count = callbackList.beginBroadcast();
267                 try {
268                     for (int i = 0; i < count; i++) {
269                         try {
270                             wrapper.call(callbackList.getBroadcastItem(i));
271                         } catch (RemoteException e) {
272                             // Safe to ignore
273                         } catch (RuntimeException e) {
274                             // We don't want an exception in one call to prevent other calls, so
275                             // we'll just log this and continue
276                             Log.e(TAG, "Caught RuntimeException in broadcast", e);
277                         }
278                     }
279                 } finally {
280                     // No matter what, we need to finish the broadcast
281                     callbackList.finishBroadcast();
282                 }
283             }
284         }
285 
286         public void loadAttachmentStatus(final long messageId, final long attachmentId,
287                 final int status, final int progress) {
288             broadcastCallback(new ServiceCallbackWrapper() {
289                 @Override
290                 public void call(IEmailServiceCallback cb) throws RemoteException {
291                     cb.loadAttachmentStatus(messageId, attachmentId, status, progress);
292                 }
293             });
294         }
295 
296         public void sendMessageStatus(final long messageId, final String subject, final int status,
297                 final int progress) {
298             broadcastCallback(new ServiceCallbackWrapper() {
299                 @Override
300                 public void call(IEmailServiceCallback cb) throws RemoteException {
301                     cb.sendMessageStatus(messageId, subject, status, progress);
302                 }
303             });
304         }
305 
306         public void syncMailboxListStatus(final long accountId, final int status,
307                 final int progress) {
308             broadcastCallback(new ServiceCallbackWrapper() {
309                 @Override
310                 public void call(IEmailServiceCallback cb) throws RemoteException {
311                     cb.syncMailboxListStatus(accountId, status, progress);
312                 }
313             });
314         }
315 
316         public void syncMailboxStatus(final long mailboxId, final int status,
317                 final int progress) {
318             broadcastCallback(new ServiceCallbackWrapper() {
319                 @Override
320                 public void call(IEmailServiceCallback cb) throws RemoteException {
321                     cb.syncMailboxStatus(mailboxId, status, progress);
322                 }
323             });
324         }
325     };
326 
327     /**
328      * Create our EmailService implementation here.
329      */
330     private final IEmailService.Stub mBinder = new IEmailService.Stub() {
331 
332         public int getApiLevel() {
333             return Api.LEVEL;
334         }
335 
336         public Bundle validate(HostAuth hostAuth) throws RemoteException {
337             return AbstractSyncService.validate(EasSyncService.class,
338                     hostAuth, ExchangeService.this);
339         }
340 
341         public Bundle autoDiscover(String userName, String password) throws RemoteException {
342             return new EasSyncService().tryAutodiscover(userName, password);
343         }
344 
345         public void startSync(long mailboxId, boolean userRequest) throws RemoteException {
346             ExchangeService exchangeService = INSTANCE;
347             if (exchangeService == null) return;
348             checkExchangeServiceServiceRunning();
349             Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
350             if (m == null) return;
351             Account acct = Account.restoreAccountWithId(exchangeService, m.mAccountKey);
352             if (acct == null) return;
353             // If this is a user request and we're being held, release the hold; this allows us to
354             // try again (the hold might have been specific to this account and released already)
355             if (userRequest) {
356                 if (onSyncDisabledHold(acct)) {
357                     releaseSyncHolds(exchangeService, AbstractSyncService.EXIT_ACCESS_DENIED, acct);
358                     log("User requested sync of account in sync disabled hold; releasing");
359                 } else if (onSecurityHold(acct)) {
360                     releaseSyncHolds(exchangeService, AbstractSyncService.EXIT_SECURITY_FAILURE,
361                             acct);
362                     log("User requested sync of account in security hold; releasing");
363                 }
364                 if (sConnectivityHold) {
365                     try {
366                         // UI is expecting the callbacks....
367                         sCallbackProxy.syncMailboxStatus(mailboxId, EmailServiceStatus.IN_PROGRESS,
368                                 0);
369                         sCallbackProxy.syncMailboxStatus(mailboxId,
370                                 EmailServiceStatus.CONNECTION_ERROR, 0);
371                     } catch (RemoteException ignore) {
372                     }
373                     return;
374                 }
375             }
376             if (m.mType == Mailbox.TYPE_OUTBOX) {
377                 // We're using SERVER_ID to indicate an error condition (it has no other use for
378                 // sent mail)  Upon request to sync the Outbox, we clear this so that all messages
379                 // are candidates for sending.
380                 ContentValues cv = new ContentValues();
381                 cv.put(SyncColumns.SERVER_ID, 0);
382                 exchangeService.getContentResolver().update(Message.CONTENT_URI,
383                     cv, WHERE_MAILBOX_KEY, new String[] {Long.toString(mailboxId)});
384                 // Clear the error state; the Outbox sync will be started from checkMailboxes
385                 exchangeService.mSyncErrorMap.remove(mailboxId);
386                 kick("start outbox");
387                 // Outbox can't be synced in EAS
388                 return;
389             } else if (!isSyncable(m)) {
390                 try {
391                     // UI may be expecting the callbacks, so send them
392                     sCallbackProxy.syncMailboxStatus(mailboxId, EmailServiceStatus.IN_PROGRESS, 0);
393                     sCallbackProxy.syncMailboxStatus(mailboxId, EmailServiceStatus.SUCCESS, 0);
394                 } catch (RemoteException ignore) {
395                     // We tried
396                 }
397                 return;
398             }
399             startManualSync(mailboxId, userRequest ? ExchangeService.SYNC_UI_REQUEST :
400                 ExchangeService.SYNC_SERVICE_START_SYNC, null);
401         }
402 
403         public void stopSync(long mailboxId) throws RemoteException {
404             stopManualSync(mailboxId);
405         }
406 
407         public void loadAttachment(long attachmentId, boolean background) throws RemoteException {
408             Attachment att = Attachment.restoreAttachmentWithId(ExchangeService.this, attachmentId);
409             log("loadAttachment " + attachmentId + ": " + att.mFileName);
410             sendMessageRequest(new PartRequest(att, null, null));
411         }
412 
413         public void updateFolderList(long accountId) throws RemoteException {
414             reloadFolderList(ExchangeService.this, accountId, false);
415         }
416 
417         public void hostChanged(long accountId) throws RemoteException {
418             ExchangeService exchangeService = INSTANCE;
419             if (exchangeService == null) return;
420             ConcurrentHashMap<Long, SyncError> syncErrorMap = exchangeService.mSyncErrorMap;
421             // Go through the various error mailboxes
422             for (long mailboxId: syncErrorMap.keySet()) {
423                 SyncError error = syncErrorMap.get(mailboxId);
424                 // If it's a login failure, look a little harder
425                 Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
426                 // If it's for the account whose host has changed, clear the error
427                 // If the mailbox is no longer around, remove the entry in the map
428                 if (m == null) {
429                     syncErrorMap.remove(mailboxId);
430                 } else if (error != null && m.mAccountKey == accountId) {
431                     error.fatal = false;
432                     error.holdEndTime = 0;
433                 }
434             }
435             // Stop any running syncs
436             exchangeService.stopAccountSyncs(accountId, true);
437             // Kick ExchangeService
438             kick("host changed");
439         }
440 
441         public void setLogging(int flags) throws RemoteException {
442             Eas.setUserDebug(flags);
443         }
444 
445         public void sendMeetingResponse(long messageId, int response) throws RemoteException {
446             sendMessageRequest(new MeetingResponseRequest(messageId, response));
447         }
448 
449         public void loadMore(long messageId) throws RemoteException {
450         }
451 
452         // The following three methods are not implemented in this version
453         public boolean createFolder(long accountId, String name) throws RemoteException {
454             return false;
455         }
456 
457         public boolean deleteFolder(long accountId, String name) throws RemoteException {
458             return false;
459         }
460 
461         public boolean renameFolder(long accountId, String oldName, String newName)
462                 throws RemoteException {
463             return false;
464         }
465 
466         public void setCallback(IEmailServiceCallback cb) throws RemoteException {
467             mCallbackList.register(cb);
468         }
469 
470         /**
471          * Delete PIM (calendar, contacts) data for the specified account
472          *
473          * @param accountId the account whose data should be deleted
474          * @throws RemoteException
475          */
476         public void deleteAccountPIMData(long accountId) throws RemoteException {
477             // Stop any running syncs
478             ExchangeService.stopAccountSyncs(accountId);
479             // Delete the data
480             ExchangeService.deleteAccountPIMData(accountId);
481         }
482 
483         public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) {
484             ExchangeService exchangeService = INSTANCE;
485             if (exchangeService == null) return 0;
486             return Search.searchMessages(exchangeService, accountId, searchParams,
487                     destMailboxId);
488         }
489     };
490 
491     /**
492      * Return a list of all Accounts in EmailProvider.  Because the result of this call may be used
493      * in account reconciliation, an exception is thrown if the result cannot be guaranteed accurate
494      * @param context the caller's context
495      * @param accounts a list that Accounts will be added into
496      * @return the list of Accounts
497      * @throws ProviderUnavailableException if the list of Accounts cannot be guaranteed valid
498      */
collectEasAccounts(Context context, AccountList accounts)499     private static AccountList collectEasAccounts(Context context, AccountList accounts) {
500         ContentResolver resolver = context.getContentResolver();
501         Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null,
502                 null);
503         // We must throw here; callers might use the information we provide for reconciliation, etc.
504         if (c == null) throw new ProviderUnavailableException();
505         try {
506             ContentValues cv = new ContentValues();
507             while (c.moveToNext()) {
508                 long hostAuthId = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN);
509                 if (hostAuthId > 0) {
510                     HostAuth ha = HostAuth.restoreHostAuthWithId(context, hostAuthId);
511                     if (ha != null && ha.mProtocol.equals("eas")) {
512                         Account account = new Account();
513                         account.restore(c);
514                         // Cache the HostAuth
515                         account.mHostAuthRecv = ha;
516                         accounts.add(account);
517                         // Fixup flags for inbox (should accept moved mail)
518                         Mailbox inbox = Mailbox.restoreMailboxOfType(context, account.mId,
519                                 Mailbox.TYPE_INBOX);
520                         if (inbox != null &&
521                                 ((inbox.mFlags & Mailbox.FLAG_ACCEPTS_MOVED_MAIL) == 0)) {
522                             cv.put(MailboxColumns.FLAGS,
523                                     inbox.mFlags | Mailbox.FLAG_ACCEPTS_MOVED_MAIL);
524                             resolver.update(
525                                     ContentUris.withAppendedId(Mailbox.CONTENT_URI, inbox.mId), cv,
526                                     null, null);
527                         }
528                     }
529                 }
530             }
531         } finally {
532             c.close();
533         }
534         return accounts;
535     }
536 
537     static class AccountList extends ArrayList<Account> {
538         private static final long serialVersionUID = 1L;
539 
540         @Override
add(Account account)541         public boolean add(Account account) {
542             // Cache the account manager account
543             account.mAmAccount = new android.accounts.Account(account.mEmailAddress,
544                     Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
545             super.add(account);
546             return true;
547         }
548 
contains(long id)549         public boolean contains(long id) {
550             for (Account account : this) {
551                 if (account.mId == id) {
552                     return true;
553                 }
554             }
555             return false;
556         }
557 
getById(long id)558         public Account getById(long id) {
559             for (Account account : this) {
560                 if (account.mId == id) {
561                     return account;
562                 }
563             }
564             return null;
565         }
566 
getByName(String accountName)567         public Account getByName(String accountName) {
568             for (Account account : this) {
569                 if (account.mEmailAddress.equalsIgnoreCase(accountName)) {
570                     return account;
571                 }
572             }
573             return null;
574         }
575     }
576 
deleteAccountPIMData(long accountId)577     public static void deleteAccountPIMData(long accountId) {
578         ExchangeService exchangeService = INSTANCE;
579         if (exchangeService == null) return;
580         Mailbox mailbox =
581             Mailbox.restoreMailboxOfType(exchangeService, accountId, Mailbox.TYPE_CONTACTS);
582         if (mailbox != null) {
583             EasSyncService service = new EasSyncService(exchangeService, mailbox);
584             ContactsSyncAdapter adapter = new ContactsSyncAdapter(service);
585             adapter.wipe();
586         }
587         mailbox =
588             Mailbox.restoreMailboxOfType(exchangeService, accountId, Mailbox.TYPE_CALENDAR);
589         if (mailbox != null) {
590             EasSyncService service = new EasSyncService(exchangeService, mailbox);
591             CalendarSyncAdapter adapter = new CalendarSyncAdapter(service);
592             adapter.wipe();
593         }
594     }
595 
onSecurityHold(Account account)596     private boolean onSecurityHold(Account account) {
597         return (account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0;
598     }
599 
onSyncDisabledHold(Account account)600     private boolean onSyncDisabledHold(Account account) {
601         return (account.mFlags & Account.FLAGS_SYNC_DISABLED) != 0;
602     }
603 
604     class AccountObserver extends ContentObserver {
605         String mSyncableEasMailboxSelector = null;
606         String mEasAccountSelector = null;
607 
608         // Runs when ExchangeService first starts
AccountObserver(Handler handler)609         public AccountObserver(Handler handler) {
610             super(handler);
611             // At startup, we want to see what EAS accounts exist and cache them
612             // TODO: Move database work out of UI thread
613             Context context = getContext();
614             synchronized (mAccountList) {
615                 try {
616                     collectEasAccounts(context, mAccountList);
617                 } catch (ProviderUnavailableException e) {
618                     // Just leave if EmailProvider is unavailable
619                     return;
620                 }
621                 // Create an account mailbox for any account without one
622                 for (Account account : mAccountList) {
623                     int cnt = Mailbox.count(context, Mailbox.CONTENT_URI, "accountKey="
624                             + account.mId, null);
625                     if (cnt == 0) {
626                         // This case handles a newly created account
627                         addAccountMailbox(account.mId);
628                     }
629                 }
630             }
631             // Run through accounts and update account hold information
632             Utility.runAsync(new Runnable() {
633                 @Override
634                 public void run() {
635                     synchronized (mAccountList) {
636                         for (Account account : mAccountList) {
637                             if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
638                                 // If we're in a security hold, and our policies are active, release
639                                 // the hold; otherwise, ping PolicyService that this account's
640                                 // policies are required
641                                 if (PolicyServiceProxy.isActive(ExchangeService.this, null)) {
642                                     PolicyServiceProxy.setAccountHoldFlag(ExchangeService.this,
643                                             account, false);
644                                     log("isActive true; release hold for " + account.mDisplayName);
645                                 } else {
646                                     PolicyServiceProxy.policiesRequired(ExchangeService.this,
647                                             account.mId);
648                                 }
649                             }
650                         }
651                     }
652                 }});
653         }
654 
655         /**
656          * Returns a String suitable for appending to a where clause that selects for all syncable
657          * mailboxes in all eas accounts
658          * @return a complex selection string that is not to be cached
659          */
getSyncableEasMailboxWhere()660         public String getSyncableEasMailboxWhere() {
661             if (mSyncableEasMailboxSelector == null) {
662                 StringBuilder sb = new StringBuilder(WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN);
663                 boolean first = true;
664                 synchronized (mAccountList) {
665                     for (Account account : mAccountList) {
666                         if (!first) {
667                             sb.append(',');
668                         } else {
669                             first = false;
670                         }
671                         sb.append(account.mId);
672                     }
673                 }
674                 sb.append(')');
675                 mSyncableEasMailboxSelector = sb.toString();
676             }
677             return mSyncableEasMailboxSelector;
678         }
679 
680         /**
681          * Returns a String suitable for appending to a where clause that selects for all eas
682          * accounts.
683          * @return a String in the form "accountKey in (a, b, c...)" that is not to be cached
684          */
getAccountKeyWhere()685         public String getAccountKeyWhere() {
686             if (mEasAccountSelector == null) {
687                 StringBuilder sb = new StringBuilder(ACCOUNT_KEY_IN);
688                 boolean first = true;
689                 synchronized (mAccountList) {
690                     for (Account account : mAccountList) {
691                         if (!first) {
692                             sb.append(',');
693                         } else {
694                             first = false;
695                         }
696                         sb.append(account.mId);
697                     }
698                 }
699                 sb.append(')');
700                 mEasAccountSelector = sb.toString();
701             }
702             return mEasAccountSelector;
703         }
704 
onAccountChanged()705         private void onAccountChanged() {
706             try {
707                 maybeStartExchangeServiceThread();
708                 Context context = getContext();
709 
710                 // A change to the list requires us to scan for deletions (stop running syncs)
711                 // At startup, we want to see what accounts exist and cache them
712                 AccountList currentAccounts = new AccountList();
713                 try {
714                     collectEasAccounts(context, currentAccounts);
715                 } catch (ProviderUnavailableException e) {
716                     // Just leave if EmailProvider is unavailable
717                     return;
718                 }
719                 synchronized (mAccountList) {
720                     for (Account account : mAccountList) {
721                         boolean accountIncomplete =
722                             (account.mFlags & Account.FLAGS_INCOMPLETE) != 0;
723                         // If the current list doesn't include this account and the account wasn't
724                         // incomplete, then this is a deletion
725                         if (!currentAccounts.contains(account.mId) && !accountIncomplete) {
726                             // The implication is that the account has been deleted; let's find out
727                             alwaysLog("Observer found deleted account: " + account.mDisplayName);
728                             // Run the reconciler (the reconciliation itself runs in the Email app)
729                             runAccountReconcilerSync(ExchangeService.this);
730                             // See if the account is still around
731                             Account deletedAccount =
732                                 Account.restoreAccountWithId(context, account.mId);
733                             if (deletedAccount != null) {
734                                 // It is; add it to our account list
735                                 alwaysLog("Account still in provider: " + account.mDisplayName);
736                                 currentAccounts.add(account);
737                             } else {
738                                 // It isn't; stop syncs and clear our selectors
739                                 alwaysLog("Account deletion confirmed: " + account.mDisplayName);
740                                 stopAccountSyncs(account.mId, true);
741                                 mSyncableEasMailboxSelector = null;
742                                 mEasAccountSelector = null;
743                             }
744                         } else {
745                             // Get the newest version of this account
746                             Account updatedAccount =
747                                 Account.restoreAccountWithId(context, account.mId);
748                             if (updatedAccount == null) continue;
749                             if (account.mSyncInterval != updatedAccount.mSyncInterval
750                                     || account.mSyncLookback != updatedAccount.mSyncLookback) {
751                                 // Set the inbox interval to the interval of the Account
752                                 // This setting should NOT affect other boxes
753                                 ContentValues cv = new ContentValues();
754                                 cv.put(MailboxColumns.SYNC_INTERVAL, updatedAccount.mSyncInterval);
755                                 getContentResolver().update(Mailbox.CONTENT_URI, cv,
756                                         WHERE_IN_ACCOUNT_AND_TYPE_INBOX, new String[] {
757                                         Long.toString(account.mId)
758                                 });
759                                 // Stop all current syncs; the appropriate ones will restart
760                                 log("Account " + account.mDisplayName + " changed; stop syncs");
761                                 stopAccountSyncs(account.mId, true);
762                             }
763 
764                             // See if this account is no longer on security hold
765                             if (onSecurityHold(account) && !onSecurityHold(updatedAccount)) {
766                                 releaseSyncHolds(ExchangeService.this,
767                                         AbstractSyncService.EXIT_SECURITY_FAILURE, account);
768                             }
769 
770                             // Put current values into our cached account
771                             account.mSyncInterval = updatedAccount.mSyncInterval;
772                             account.mSyncLookback = updatedAccount.mSyncLookback;
773                             account.mFlags = updatedAccount.mFlags;
774                         }
775                     }
776                     // Look for new accounts
777                     for (Account account : currentAccounts) {
778                         if (!mAccountList.contains(account.mId)) {
779                             // Don't forget to cache the HostAuth
780                             HostAuth ha = HostAuth.restoreHostAuthWithId(getContext(),
781                                     account.mHostAuthKeyRecv);
782                             if (ha == null) continue;
783                             account.mHostAuthRecv = ha;
784                             // This is an addition; create our magic hidden mailbox...
785                             log("Account observer found new account: " + account.mDisplayName);
786                             addAccountMailbox(account.mId);
787                             mAccountList.add(account);
788                             mSyncableEasMailboxSelector = null;
789                             mEasAccountSelector = null;
790                         }
791                     }
792                     // Finally, make sure our account list is up to date
793                     mAccountList.clear();
794                     mAccountList.addAll(currentAccounts);
795                 }
796 
797                 // See if there's anything to do...
798                 kick("account changed");
799             } catch (ProviderUnavailableException e) {
800                 alwaysLog("Observer failed; provider unavailable");
801             }
802         }
803 
804         @Override
onChange(boolean selfChange)805         public void onChange(boolean selfChange) {
806             new Thread(new Runnable() {
807                public void run() {
808                    onAccountChanged();
809                 }}, "Account Observer").start();
810         }
811 
addAccountMailbox(long acctId)812         private void addAccountMailbox(long acctId) {
813             Account acct = Account.restoreAccountWithId(getContext(), acctId);
814             Mailbox main = new Mailbox();
815             main.mDisplayName = Eas.ACCOUNT_MAILBOX_PREFIX;
816             main.mServerId = Eas.ACCOUNT_MAILBOX_PREFIX + System.nanoTime();
817             main.mAccountKey = acct.mId;
818             main.mType = Mailbox.TYPE_EAS_ACCOUNT_MAILBOX;
819             main.mSyncInterval = Mailbox.CHECK_INTERVAL_PUSH;
820             main.mFlagVisible = false;
821             main.save(getContext());
822             log("Initializing account: " + acct.mDisplayName);
823         }
824 
825     }
826 
827     /**
828      * Register a specific Calendar's data observer; we need to recognize when the SYNC_EVENTS
829      * column has changed (when sync has turned off or on)
830      * @param account the Account whose Calendar we're observing
831      */
registerCalendarObserver(Account account)832     private void registerCalendarObserver(Account account) {
833         // Get a new observer
834         CalendarObserver observer = new CalendarObserver(mHandler, account);
835         if (observer.mCalendarId != 0) {
836             // If we find the Calendar (and we'd better) register it and store it in the map
837             mCalendarObservers.put(account.mId, observer);
838             mResolver.registerContentObserver(
839                     ContentUris.withAppendedId(Calendars.CONTENT_URI, observer.mCalendarId), false,
840                     observer);
841         }
842     }
843 
844     /**
845      * Unregister all CalendarObserver's
846      */
unregisterCalendarObservers()847     static public void unregisterCalendarObservers() {
848         ExchangeService exchangeService = INSTANCE;
849         if (exchangeService == null) return;
850         ContentResolver resolver = exchangeService.mResolver;
851         for (CalendarObserver observer: exchangeService.mCalendarObservers.values()) {
852             resolver.unregisterContentObserver(observer);
853         }
854         exchangeService.mCalendarObservers.clear();
855     }
856 
857     /**
858      * Return the syncable state of an account's calendar, as determined by the sync_events column
859      * of our Calendar (from CalendarProvider2)
860      * Note that the current state of sync_events is cached in our CalendarObserver
861      * @param accountId the id of the account whose calendar we are checking
862      * @return whether or not syncing of events is enabled
863      */
isCalendarEnabled(long accountId)864     private boolean isCalendarEnabled(long accountId) {
865         CalendarObserver observer = mCalendarObservers.get(accountId);
866         if (observer != null) {
867             return (observer.mSyncEvents == 1);
868         }
869         // If there's no observer, there's no Calendar in CalendarProvider2, so we return true
870         // to allow Calendar creation
871         return true;
872     }
873 
874     private class CalendarObserver extends ContentObserver {
875         long mAccountId;
876         long mCalendarId;
877         long mSyncEvents;
878         String mAccountName;
879 
CalendarObserver(Handler handler, Account account)880         public CalendarObserver(Handler handler, Account account) {
881             super(handler);
882             mAccountId = account.mId;
883             mAccountName = account.mEmailAddress;
884 
885             // Find the Calendar for this account
886             Cursor c = mResolver.query(Calendars.CONTENT_URI,
887                     new String[] {Calendars._ID, Calendars.SYNC_EVENTS},
888                     CalendarSyncAdapter.CALENDAR_SELECTION,
889                     new String[] {account.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE},
890                     null);
891             if (c != null) {
892                 // Save its id and its sync events status
893                 try {
894                     if (c.moveToFirst()) {
895                         mCalendarId = c.getLong(0);
896                         mSyncEvents = c.getLong(1);
897                     }
898                 } finally {
899                     c.close();
900                 }
901             }
902         }
903 
904         @Override
onChange(boolean selfChange)905         public synchronized void onChange(boolean selfChange) {
906             // See if the user has changed syncing of our calendar
907             if (!selfChange) {
908                 new Thread(new Runnable() {
909                     public void run() {
910                         try {
911                             Cursor c = mResolver.query(Calendars.CONTENT_URI,
912                                     new String[] {Calendars.SYNC_EVENTS}, Calendars._ID + "=?",
913                                     new String[] {Long.toString(mCalendarId)}, null);
914                             if (c == null) return;
915                             // Get its sync events; if it's changed, we've got work to do
916                             try {
917                                 if (c.moveToFirst()) {
918                                     long newSyncEvents = c.getLong(0);
919                                     if (newSyncEvents != mSyncEvents) {
920                                         log("_sync_events changed for calendar in " + mAccountName);
921                                         Mailbox mailbox = Mailbox.restoreMailboxOfType(INSTANCE,
922                                                 mAccountId, Mailbox.TYPE_CALENDAR);
923                                         // Sanity check for mailbox deletion
924                                         if (mailbox == null) return;
925                                         ContentValues cv = new ContentValues();
926                                         if (newSyncEvents == 0) {
927                                             // When sync is disabled, we're supposed to delete
928                                             // all events in the calendar
929                                             log("Deleting events and setting syncKey to 0 for " +
930                                                     mAccountName);
931                                             // First, stop any sync that's ongoing
932                                             stopManualSync(mailbox.mId);
933                                             // Set the syncKey to 0 (reset)
934                                             EasSyncService service =
935                                                 new EasSyncService(INSTANCE, mailbox);
936                                             CalendarSyncAdapter adapter =
937                                                 new CalendarSyncAdapter(service);
938                                             try {
939                                                 adapter.setSyncKey("0", false);
940                                             } catch (IOException e) {
941                                                 // The provider can't be reached; nothing to be done
942                                             }
943                                             // Reset the sync key locally and stop syncing
944                                             cv.put(Mailbox.SYNC_KEY, "0");
945                                             cv.put(Mailbox.SYNC_INTERVAL,
946                                                     Mailbox.CHECK_INTERVAL_NEVER);
947                                             mResolver.update(ContentUris.withAppendedId(
948                                                     Mailbox.CONTENT_URI, mailbox.mId), cv, null,
949                                                     null);
950                                             // Delete all events using the sync adapter
951                                             // parameter so that the deletion is only local
952                                             Uri eventsAsSyncAdapter =
953                                                 CalendarSyncAdapter.asSyncAdapter(
954                                                     Events.CONTENT_URI,
955                                                     mAccountName,
956                                                     Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
957                                             mResolver.delete(eventsAsSyncAdapter, WHERE_CALENDAR_ID,
958                                                     new String[] {Long.toString(mCalendarId)});
959                                         } else {
960                                             // Make this a push mailbox and kick; this will start
961                                             // a resync of the Calendar; the account mailbox will
962                                             // ping on this during the next cycle of the ping loop
963                                             cv.put(Mailbox.SYNC_INTERVAL,
964                                                     Mailbox.CHECK_INTERVAL_PUSH);
965                                             mResolver.update(ContentUris.withAppendedId(
966                                                     Mailbox.CONTENT_URI, mailbox.mId), cv, null,
967                                                     null);
968                                             kick("calendar sync changed");
969                                         }
970 
971                                         // Save away the new value
972                                         mSyncEvents = newSyncEvents;
973                                     }
974                                 }
975                             } finally {
976                                 c.close();
977                             }
978                         } catch (ProviderUnavailableException e) {
979                             Log.w(TAG, "Observer failed; provider unavailable");
980                         }
981                     }}, "Calendar Observer").start();
982             }
983         }
984     }
985 
986     private class MailboxObserver extends ContentObserver {
MailboxObserver(Handler handler)987         public MailboxObserver(Handler handler) {
988             super(handler);
989         }
990 
991         @Override
onChange(boolean selfChange)992         public void onChange(boolean selfChange) {
993             // See if there's anything to do...
994             if (!selfChange) {
995                 kick("mailbox changed");
996             }
997         }
998     }
999 
1000     private class SyncedMessageObserver extends ContentObserver {
1001         Intent syncAlarmIntent = new Intent(INSTANCE, EmailSyncAlarmReceiver.class);
1002         PendingIntent syncAlarmPendingIntent =
1003             PendingIntent.getBroadcast(INSTANCE, 0, syncAlarmIntent, 0);
1004         AlarmManager alarmManager = (AlarmManager)INSTANCE.getSystemService(Context.ALARM_SERVICE);
1005 
SyncedMessageObserver(Handler handler)1006         public SyncedMessageObserver(Handler handler) {
1007             super(handler);
1008         }
1009 
1010         @Override
onChange(boolean selfChange)1011         public void onChange(boolean selfChange) {
1012             alarmManager.set(AlarmManager.RTC_WAKEUP,
1013                     System.currentTimeMillis() + 10*SECONDS, syncAlarmPendingIntent);
1014         }
1015     }
1016 
callback()1017     static public IEmailServiceCallback callback() {
1018         return sCallbackProxy;
1019     }
1020 
getAccountById(long accountId)1021     static public Account getAccountById(long accountId) {
1022         ExchangeService exchangeService = INSTANCE;
1023         if (exchangeService != null) {
1024             AccountList accountList = exchangeService.mAccountList;
1025             synchronized (accountList) {
1026                 return accountList.getById(accountId);
1027             }
1028         }
1029         return null;
1030     }
1031 
getAccountByName(String accountName)1032     static public Account getAccountByName(String accountName) {
1033         ExchangeService exchangeService = INSTANCE;
1034         if (exchangeService != null) {
1035             AccountList accountList = exchangeService.mAccountList;
1036             synchronized (accountList) {
1037                 return accountList.getByName(accountName);
1038             }
1039         }
1040         return null;
1041     }
1042 
getEasAccountSelector()1043     static public String getEasAccountSelector() {
1044         ExchangeService exchangeService = INSTANCE;
1045         if (exchangeService != null && exchangeService.mAccountObserver != null) {
1046             return exchangeService.mAccountObserver.getAccountKeyWhere();
1047         }
1048         return null;
1049     }
1050 
1051     public class SyncStatus {
1052         static public final int NOT_RUNNING = 0;
1053         static public final int DIED = 1;
1054         static public final int SYNC = 2;
1055         static public final int IDLE = 3;
1056     }
1057 
1058     /*package*/ class SyncError {
1059         int reason;
1060         boolean fatal = false;
1061         long holdDelay = 15*SECONDS;
1062         long holdEndTime = System.currentTimeMillis() + holdDelay;
1063 
SyncError(int _reason, boolean _fatal)1064         SyncError(int _reason, boolean _fatal) {
1065             reason = _reason;
1066             fatal = _fatal;
1067         }
1068 
1069         /**
1070          * We double the holdDelay from 15 seconds through 4 mins
1071          */
escalate()1072         void escalate() {
1073             if (holdDelay < HOLD_DELAY_MAXIMUM) {
1074                 holdDelay *= 2;
1075             }
1076             holdEndTime = System.currentTimeMillis() + holdDelay;
1077         }
1078     }
1079 
logSyncHolds()1080     private void logSyncHolds() {
1081         if (Eas.USER_LOG) {
1082             log("Sync holds:");
1083             long time = System.currentTimeMillis();
1084             for (long mailboxId : mSyncErrorMap.keySet()) {
1085                 Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId);
1086                 if (m == null) {
1087                     log("Mailbox " + mailboxId + " no longer exists");
1088                 } else {
1089                     SyncError error = mSyncErrorMap.get(mailboxId);
1090                     if (error != null) {
1091                         log("Mailbox " + m.mDisplayName + ", error = " + error.reason
1092                                 + ", fatal = " + error.fatal);
1093                         if (error.holdEndTime > 0) {
1094                             log("Hold ends in " + ((error.holdEndTime - time) / 1000) + "s");
1095                         }
1096                     }
1097                 }
1098             }
1099         }
1100     }
1101 
1102     /**
1103      * Release security holds for the specified account
1104      * @param account the account whose Mailboxes should be released from security hold
1105      */
releaseSecurityHold(Account account)1106     static public void releaseSecurityHold(Account account) {
1107         ExchangeService exchangeService = INSTANCE;
1108         if (exchangeService != null) {
1109             exchangeService.releaseSyncHolds(INSTANCE, AbstractSyncService.EXIT_SECURITY_FAILURE,
1110                     account);
1111         }
1112     }
1113 
1114     /**
1115      * Release a specific type of hold (the reason) for the specified Account; if the account
1116      * is null, mailboxes from all accounts with the specified hold will be released
1117      * @param reason the reason for the SyncError (AbstractSyncService.EXIT_XXX)
1118      * @param account an Account whose mailboxes should be released (or all if null)
1119      * @return whether or not any mailboxes were released
1120      */
releaseSyncHolds(Context context, int reason, Account account)1121     /*package*/ boolean releaseSyncHolds(Context context, int reason, Account account) {
1122         boolean holdWasReleased = releaseSyncHoldsImpl(context, reason, account);
1123         kick("security release");
1124         return holdWasReleased;
1125     }
1126 
releaseSyncHoldsImpl(Context context, int reason, Account account)1127     private boolean releaseSyncHoldsImpl(Context context, int reason, Account account) {
1128         boolean holdWasReleased = false;
1129         for (long mailboxId: mSyncErrorMap.keySet()) {
1130             if (account != null) {
1131                 Mailbox m = Mailbox.restoreMailboxWithId(context, mailboxId);
1132                 if (m == null) {
1133                     mSyncErrorMap.remove(mailboxId);
1134                 } else if (m.mAccountKey != account.mId) {
1135                     continue;
1136                 }
1137             }
1138             SyncError error = mSyncErrorMap.get(mailboxId);
1139             if (error != null && error.reason == reason) {
1140                 mSyncErrorMap.remove(mailboxId);
1141                 holdWasReleased = true;
1142             }
1143         }
1144         return holdWasReleased;
1145     }
1146 
1147     /**
1148      * Reconcile Exchange accounts with AccountManager (asynchronous)
1149      * @param context the caller's Context
1150      */
reconcileAccounts(final Context context)1151     public static void reconcileAccounts(final Context context) {
1152         Utility.runAsync(new Runnable() {
1153             @Override
1154             public void run() {
1155                 ExchangeService exchangeService = INSTANCE;
1156                 if (exchangeService != null) {
1157                     exchangeService.runAccountReconcilerSync(context);
1158                 }
1159             }});
1160     }
1161 
1162     /**
1163      * Blocking call to the account reconciler
1164      */
runAccountReconcilerSync(Context context)1165     public static void runAccountReconcilerSync(Context context) {
1166         alwaysLog("Reconciling accounts...");
1167         new AccountServiceProxy(context).reconcileAccounts(
1168                 HostAuth.SCHEME_EAS, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
1169     }
1170 
log(String str)1171     public static void log(String str) {
1172         log(TAG, str);
1173     }
1174 
log(String tag, String str)1175     public static void log(String tag, String str) {
1176         if (Eas.USER_LOG) {
1177             Log.d(tag, str);
1178             if (Eas.FILE_LOG) {
1179                 FileLogger.log(tag, str);
1180             }
1181         }
1182     }
1183 
alwaysLog(String str)1184     public static void alwaysLog(String str) {
1185         if (!Eas.USER_LOG) {
1186             Log.d(TAG, str);
1187         } else {
1188             log(str);
1189         }
1190     }
1191 
1192     /**
1193      * EAS requires a unique device id, so that sync is possible from a variety of different
1194      * devices (e.g. the syncKey is specific to a device)  If we're on an emulator or some other
1195      * device that doesn't provide one, we can create it as "device".
1196      * This would work on a real device as well, but it would be better to use the "real" id if
1197      * it's available
1198      */
getDeviceId(Context context)1199     static public String getDeviceId(Context context) throws IOException {
1200         if (sDeviceId == null) {
1201             sDeviceId = new AccountServiceProxy(context).getDeviceId();
1202             alwaysLog("Received deviceId from Email app: " + sDeviceId);
1203         }
1204         return sDeviceId;
1205     }
1206 
1207     @Override
onBind(Intent arg0)1208     public IBinder onBind(Intent arg0) {
1209         return mBinder;
1210     }
1211 
1212     static public ConnPerRoute sConnPerRoute = new ConnPerRoute() {
1213         public int getMaxForRoute(HttpRoute route) {
1214             return 8;
1215         }
1216     };
1217 
getClientConnectionManager()1218     static public synchronized EmailClientConnectionManager getClientConnectionManager() {
1219         if (sClientConnectionManager == null) {
1220             // After two tries, kill the process.  Most likely, this will happen in the background
1221             // The service will restart itself after about 5 seconds
1222             if (sClientConnectionManagerShutdownCount > MAX_CLIENT_CONNECTION_MANAGER_SHUTDOWNS) {
1223                 alwaysLog("Shutting down process to unblock threads");
1224                 Process.killProcess(Process.myPid());
1225             }
1226             HttpParams params = new BasicHttpParams();
1227             params.setIntParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, 25);
1228             params.setParameter(ConnManagerPNames.MAX_CONNECTIONS_PER_ROUTE, sConnPerRoute);
1229             sClientConnectionManager = EmailClientConnectionManager.newInstance(params);
1230         }
1231         // Null is a valid return result if we get an exception
1232         return sClientConnectionManager;
1233     }
1234 
shutdownConnectionManager()1235     static private synchronized void shutdownConnectionManager() {
1236         if (sClientConnectionManager != null) {
1237             log("Shutting down ClientConnectionManager");
1238             sClientConnectionManager.shutdown();
1239             sClientConnectionManagerShutdownCount++;
1240             sClientConnectionManager = null;
1241         }
1242     }
1243 
stopAccountSyncs(long acctId)1244     public static void stopAccountSyncs(long acctId) {
1245         ExchangeService exchangeService = INSTANCE;
1246         if (exchangeService != null) {
1247             exchangeService.stopAccountSyncs(acctId, true);
1248         }
1249     }
1250 
stopAccountSyncs(long acctId, boolean includeAccountMailbox)1251     private void stopAccountSyncs(long acctId, boolean includeAccountMailbox) {
1252         synchronized (sSyncLock) {
1253             List<Long> deletedBoxes = new ArrayList<Long>();
1254             for (Long mid : mServiceMap.keySet()) {
1255                 Mailbox box = Mailbox.restoreMailboxWithId(this, mid);
1256                 if (box != null) {
1257                     if (box.mAccountKey == acctId) {
1258                         if (!includeAccountMailbox &&
1259                                 box.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
1260                             AbstractSyncService svc = mServiceMap.get(mid);
1261                             if (svc != null) {
1262                                 svc.stop();
1263                             }
1264                             continue;
1265                         }
1266                         AbstractSyncService svc = mServiceMap.get(mid);
1267                         if (svc != null) {
1268                             svc.stop();
1269                             Thread t = svc.mThread;
1270                             if (t != null) {
1271                                 t.interrupt();
1272                             }
1273                         }
1274                         deletedBoxes.add(mid);
1275                     }
1276                 }
1277             }
1278             for (Long mid : deletedBoxes) {
1279                 releaseMailbox(mid);
1280             }
1281         }
1282     }
1283 
reloadFolderListFailed(long accountId)1284     static private void reloadFolderListFailed(long accountId) {
1285         try {
1286             callback().syncMailboxListStatus(accountId,
1287                     EmailServiceStatus.ACCOUNT_UNINITIALIZED, 0);
1288         } catch (RemoteException e1) {
1289             // Don't care if this fails
1290         }
1291     }
1292 
reloadFolderList(Context context, long accountId, boolean force)1293     static public void reloadFolderList(Context context, long accountId, boolean force) {
1294         ExchangeService exchangeService = INSTANCE;
1295         if (exchangeService == null) return;
1296         Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI,
1297                 Mailbox.CONTENT_PROJECTION, MailboxColumns.ACCOUNT_KEY + "=? AND " +
1298                 MailboxColumns.TYPE + "=?",
1299                 new String[] {Long.toString(accountId),
1300                     Long.toString(Mailbox.TYPE_EAS_ACCOUNT_MAILBOX)}, null);
1301         try {
1302             if (c.moveToFirst()) {
1303                 synchronized(sSyncLock) {
1304                     Mailbox mailbox = new Mailbox();
1305                     mailbox.restore(c);
1306                     Account acct = Account.restoreAccountWithId(context, accountId);
1307                     if (acct == null) {
1308                         reloadFolderListFailed(accountId);
1309                         return;
1310                     }
1311                     String syncKey = acct.mSyncKey;
1312                     // No need to reload the list if we don't have one
1313                     if (!force && (syncKey == null || syncKey.equals("0"))) {
1314                         reloadFolderListFailed(accountId);
1315                         return;
1316                     }
1317 
1318                     // Change all ping/push boxes to push/hold
1319                     ContentValues cv = new ContentValues();
1320                     cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH_HOLD);
1321                     context.getContentResolver().update(Mailbox.CONTENT_URI, cv,
1322                             WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX,
1323                             new String[] {Long.toString(accountId)});
1324                     log("Set push/ping boxes to push/hold");
1325 
1326                     long id = mailbox.mId;
1327                     AbstractSyncService svc = exchangeService.mServiceMap.get(id);
1328                     // Tell the service we're done
1329                     if (svc != null) {
1330                         synchronized (svc.getSynchronizer()) {
1331                             svc.stop();
1332                             // Interrupt the thread so that it can stop
1333                             Thread thread = svc.mThread;
1334                             if (thread != null) {
1335                                 thread.setName(thread.getName() + " (Stopped)");
1336                                 thread.interrupt();
1337                             }
1338                         }
1339                         // Abandon the service
1340                         exchangeService.releaseMailbox(id);
1341                         // And have it start naturally
1342                         kick("reload folder list");
1343                     }
1344                 }
1345             }
1346         } finally {
1347             c.close();
1348         }
1349     }
1350 
1351     /**
1352      * Informs ExchangeService that an account has a new folder list; as a result, any existing
1353      * folder might have become invalid.  Therefore, we act as if the account has been deleted, and
1354      * then we reinitialize it.
1355      *
1356      * @param acctId
1357      */
stopNonAccountMailboxSyncsForAccount(long acctId)1358     static public void stopNonAccountMailboxSyncsForAccount(long acctId) {
1359         ExchangeService exchangeService = INSTANCE;
1360         if (exchangeService != null) {
1361             exchangeService.stopAccountSyncs(acctId, false);
1362             kick("reload folder list");
1363         }
1364     }
1365 
acquireWakeLock(long id)1366     private void acquireWakeLock(long id) {
1367         synchronized (mWakeLocks) {
1368             Boolean lock = mWakeLocks.get(id);
1369             if (lock == null) {
1370                 if (mWakeLock == null) {
1371                     PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
1372                     mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MAIL_SERVICE");
1373                     mWakeLock.acquire();
1374                     //log("+WAKE LOCK ACQUIRED");
1375                 }
1376                 mWakeLocks.put(id, true);
1377              }
1378         }
1379     }
1380 
releaseWakeLock(long id)1381     private void releaseWakeLock(long id) {
1382         synchronized (mWakeLocks) {
1383             Boolean lock = mWakeLocks.get(id);
1384             if (lock != null) {
1385                 mWakeLocks.remove(id);
1386                 if (mWakeLocks.isEmpty()) {
1387                     if (mWakeLock != null) {
1388                         mWakeLock.release();
1389                     }
1390                     mWakeLock = null;
1391                     //log("+WAKE LOCK RELEASED");
1392                 } else {
1393                 }
1394             }
1395         }
1396     }
1397 
alarmOwner(long id)1398     static public String alarmOwner(long id) {
1399         if (id == EXTRA_MAILBOX_ID) {
1400             return "ExchangeService";
1401         } else {
1402             String name = Long.toString(id);
1403             if (Eas.USER_LOG && INSTANCE != null) {
1404                 Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, id);
1405                 if (m != null) {
1406                     name = m.mDisplayName + '(' + m.mAccountKey + ')';
1407                 }
1408             }
1409             return "Mailbox " + name;
1410         }
1411     }
1412 
clearAlarm(long id)1413     private void clearAlarm(long id) {
1414         synchronized (mPendingIntents) {
1415             PendingIntent pi = mPendingIntents.get(id);
1416             if (pi != null) {
1417                 AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
1418                 alarmManager.cancel(pi);
1419                 //log("+Alarm cleared for " + alarmOwner(id));
1420                 mPendingIntents.remove(id);
1421             }
1422         }
1423     }
1424 
setAlarm(long id, long millis)1425     private void setAlarm(long id, long millis) {
1426         synchronized (mPendingIntents) {
1427             PendingIntent pi = mPendingIntents.get(id);
1428             if (pi == null) {
1429                 Intent i = new Intent(this, MailboxAlarmReceiver.class);
1430                 i.putExtra("mailbox", id);
1431                 i.setData(Uri.parse("Box" + id));
1432                 pi = PendingIntent.getBroadcast(this, 0, i, 0);
1433                 mPendingIntents.put(id, pi);
1434 
1435                 AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
1436                 alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + millis, pi);
1437                 //log("+Alarm set for " + alarmOwner(id) + ", " + millis/1000 + "s");
1438             }
1439         }
1440     }
1441 
clearAlarms()1442     private void clearAlarms() {
1443         AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
1444         synchronized (mPendingIntents) {
1445             for (PendingIntent pi : mPendingIntents.values()) {
1446                 alarmManager.cancel(pi);
1447             }
1448             mPendingIntents.clear();
1449         }
1450     }
1451 
runAwake(long id)1452     static public void runAwake(long id) {
1453         ExchangeService exchangeService = INSTANCE;
1454         if (exchangeService != null) {
1455             exchangeService.acquireWakeLock(id);
1456             exchangeService.clearAlarm(id);
1457         }
1458     }
1459 
runAsleep(long id, long millis)1460     static public void runAsleep(long id, long millis) {
1461         ExchangeService exchangeService = INSTANCE;
1462         if (exchangeService != null) {
1463             exchangeService.setAlarm(id, millis);
1464             exchangeService.releaseWakeLock(id);
1465         }
1466     }
1467 
clearWatchdogAlarm(long id)1468     static public void clearWatchdogAlarm(long id) {
1469         ExchangeService exchangeService = INSTANCE;
1470         if (exchangeService != null) {
1471             exchangeService.clearAlarm(id);
1472         }
1473     }
1474 
setWatchdogAlarm(long id, long millis)1475     static public void setWatchdogAlarm(long id, long millis) {
1476         ExchangeService exchangeService = INSTANCE;
1477         if (exchangeService != null) {
1478             exchangeService.setAlarm(id, millis);
1479         }
1480     }
1481 
alert(Context context, final long id)1482     static public void alert(Context context, final long id) {
1483         final ExchangeService exchangeService = INSTANCE;
1484         checkExchangeServiceServiceRunning();
1485         if (id < 0) {
1486             log("ExchangeService alert");
1487             kick("ping ExchangeService");
1488         } else if (exchangeService == null) {
1489             context.startService(new Intent(context, ExchangeService.class));
1490         } else {
1491             final AbstractSyncService service = exchangeService.mServiceMap.get(id);
1492             if (service != null) {
1493                 // Handle alerts in a background thread, as we are typically called from a
1494                 // broadcast receiver, and are therefore running in the UI thread
1495                 String threadName = "ExchangeService Alert: ";
1496                 if (service.mMailbox != null) {
1497                     threadName += service.mMailbox.mDisplayName;
1498                 }
1499                 new Thread(new Runnable() {
1500                    public void run() {
1501                        Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, id);
1502                        if (m != null) {
1503                            // We ignore drafts completely (doesn't sync).  Changes in Outbox are
1504                            // handled in the checkMailboxes loop, so we can ignore these pings.
1505                            if (Eas.USER_LOG) {
1506                                Log.d(TAG, "Alert for mailbox " + id + " (" + m.mDisplayName + ")");
1507                            }
1508                            if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX) {
1509                                String[] args = new String[] {Long.toString(m.mId)};
1510                                ContentResolver resolver = INSTANCE.mResolver;
1511                                resolver.delete(Message.DELETED_CONTENT_URI, WHERE_MAILBOX_KEY,
1512                                        args);
1513                                resolver.delete(Message.UPDATED_CONTENT_URI, WHERE_MAILBOX_KEY,
1514                                        args);
1515                                return;
1516                            }
1517                            service.mAccount = Account.restoreAccountWithId(INSTANCE, m.mAccountKey);
1518                            service.mMailbox = m;
1519                            // Send the alarm to the sync service
1520                            if (!service.alarm()) {
1521                                // A false return means that we were forced to interrupt the thread
1522                                // In this case, we release the mailbox so that we can start another
1523                                // thread to do the work
1524                                log("Alarm failed; releasing mailbox");
1525                                synchronized(sSyncLock) {
1526                                    exchangeService.releaseMailbox(id);
1527                                }
1528                                // Shutdown the connection manager; this should close all of our
1529                                // sockets and generate IOExceptions all around.
1530                                ExchangeService.shutdownConnectionManager();
1531                            }
1532                        }
1533                     }}, threadName).start();
1534             }
1535         }
1536     }
1537 
1538     public class ConnectivityReceiver extends BroadcastReceiver {
1539         @Override
onReceive(Context context, Intent intent)1540         public void onReceive(Context context, Intent intent) {
1541             if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
1542                 Bundle b = intent.getExtras();
1543                 if (b != null) {
1544                     NetworkInfo a = (NetworkInfo)b.get(ConnectivityManager.EXTRA_NETWORK_INFO);
1545                     String info = "Connectivity alert for " + a.getTypeName();
1546                     State state = a.getState();
1547                     if (state == State.CONNECTED) {
1548                         info += " CONNECTED";
1549                         log(info);
1550                         synchronized (sConnectivityLock) {
1551                             sConnectivityLock.notifyAll();
1552                         }
1553                         kick("connected");
1554                     } else if (state == State.DISCONNECTED) {
1555                         info += " DISCONNECTED";
1556                         log(info);
1557                         kick("disconnected");
1558                     }
1559                 }
1560             } else if (intent.getAction().equals(
1561                     ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED)) {
1562                 ConnectivityManager cm =
1563                         (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
1564                 mBackgroundData = cm.getBackgroundDataSetting();
1565                 // If background data is now on, we want to kick ExchangeService
1566                 if (mBackgroundData) {
1567                     kick("background data on");
1568                     log("Background data on; restart syncs");
1569                 // Otherwise, stop all syncs
1570                 } else {
1571                     log("Background data off: stop all syncs");
1572                     EmailAsyncTask.runAsyncParallel(new Runnable() {
1573                         @Override
1574                         public void run() {
1575                             synchronized (mAccountList) {
1576                                 for (Account account : mAccountList)
1577                                     ExchangeService.stopAccountSyncs(account.mId);
1578                             }
1579                         }});
1580                 }
1581             }
1582         }
1583     }
1584 
1585     /**
1586      * Starts a service thread and enters it into the service map
1587      * This is the point of instantiation of all sync threads
1588      * @param service the service to start
1589      * @param m the Mailbox on which the service will operate
1590      */
startServiceThread(AbstractSyncService service, Mailbox m)1591     private void startServiceThread(AbstractSyncService service, Mailbox m) {
1592         if (m == null) return;
1593         synchronized (sSyncLock) {
1594             String mailboxName = m.mDisplayName;
1595             String accountName = service.mAccount.mDisplayName;
1596             Thread thread = new Thread(service, mailboxName + "[" + accountName + "]");
1597             log("Starting thread for " + mailboxName + " in account " + accountName);
1598             thread.start();
1599             mServiceMap.put(m.mId, service);
1600             runAwake(m.mId);
1601             if ((m.mServerId != null) && !m.mServerId.startsWith(Eas.ACCOUNT_MAILBOX_PREFIX)) {
1602                 stopPing(m.mAccountKey);
1603             }
1604         }
1605     }
1606 
1607     /**
1608      * Stop any ping in progress for the given account
1609      * @param accountId
1610      */
stopPing(long accountId)1611     private void stopPing(long accountId) {
1612         // Go through our active mailboxes looking for the right one
1613         synchronized (sSyncLock) {
1614             for (long mailboxId: mServiceMap.keySet()) {
1615                 Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId);
1616                 if (m != null) {
1617                     String serverId = m.mServerId;
1618                     if (m.mAccountKey == accountId && serverId != null &&
1619                             serverId.startsWith(Eas.ACCOUNT_MAILBOX_PREFIX)) {
1620                         // Here's our account mailbox; reset him (stopping pings)
1621                         AbstractSyncService svc = mServiceMap.get(mailboxId);
1622                         svc.reset();
1623                     }
1624                 }
1625             }
1626         }
1627     }
1628 
requestSync(Mailbox m, int reason, Request req)1629     private void requestSync(Mailbox m, int reason, Request req) {
1630         // Don't sync if there's no connectivity
1631         if (sConnectivityHold || (m == null) || sStop) {
1632             if (reason >= SYNC_CALLBACK_START) {
1633                 try {
1634                     sCallbackProxy.syncMailboxStatus(m.mId, EmailServiceStatus.CONNECTION_ERROR, 0);
1635                 } catch (RemoteException e) {
1636                     // We tried...
1637                 }
1638             }
1639             return;
1640         }
1641         synchronized (sSyncLock) {
1642             Account acct = Account.restoreAccountWithId(this, m.mAccountKey);
1643             if (acct != null) {
1644                 // Always make sure there's not a running instance of this service
1645                 AbstractSyncService service = mServiceMap.get(m.mId);
1646                 if (service == null) {
1647                     service = new EasSyncService(this, m);
1648                     if (!((EasSyncService)service).mIsValid) return;
1649                     service.mSyncReason = reason;
1650                     if (req != null) {
1651                         service.addRequest(req);
1652                     }
1653                     startServiceThread(service, m);
1654                 }
1655             }
1656         }
1657     }
1658 
stopServiceThreads()1659     private void stopServiceThreads() {
1660         synchronized (sSyncLock) {
1661             ArrayList<Long> toStop = new ArrayList<Long>();
1662 
1663             // Keep track of which services to stop
1664             for (Long mailboxId : mServiceMap.keySet()) {
1665                 toStop.add(mailboxId);
1666             }
1667 
1668             // Shut down all of those running services
1669             for (Long mailboxId : toStop) {
1670                 AbstractSyncService svc = mServiceMap.get(mailboxId);
1671                 if (svc != null) {
1672                     log("Stopping " + svc.mAccount.mDisplayName + '/' + svc.mMailbox.mDisplayName);
1673                     svc.stop();
1674                     if (svc.mThread != null) {
1675                         svc.mThread.interrupt();
1676                     }
1677                 }
1678                 releaseWakeLock(mailboxId);
1679             }
1680         }
1681     }
1682 
waitForConnectivity()1683     private void waitForConnectivity() {
1684         boolean waiting = false;
1685         ConnectivityManager cm =
1686             (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
1687         while (!sStop) {
1688             NetworkInfo info = cm.getActiveNetworkInfo();
1689             if (info != null) {
1690                 mNetworkInfo = info;
1691                 // We're done if there's an active network
1692                 if (waiting) {
1693                     // If we've been waiting, release any I/O error holds
1694                     releaseSyncHolds(this, AbstractSyncService.EXIT_IO_ERROR, null);
1695                     // And log what's still being held
1696                     logSyncHolds();
1697                 }
1698                 return;
1699             } else {
1700                 // If this is our first time through the loop, shut down running service threads
1701                 if (!waiting) {
1702                     waiting = true;
1703                     stopServiceThreads();
1704                 }
1705                 // Wait until a network is connected (or 10 mins), but let the device sleep
1706                 // We'll set an alarm just in case we don't get notified (bugs happen)
1707                 synchronized (sConnectivityLock) {
1708                     runAsleep(EXTRA_MAILBOX_ID, CONNECTIVITY_WAIT_TIME+5*SECONDS);
1709                     try {
1710                         log("Connectivity lock...");
1711                         sConnectivityHold = true;
1712                         sConnectivityLock.wait(CONNECTIVITY_WAIT_TIME);
1713                         log("Connectivity lock released...");
1714                     } catch (InterruptedException e) {
1715                         // This is fine; we just go around the loop again
1716                     } finally {
1717                         sConnectivityHold = false;
1718                     }
1719                     runAwake(EXTRA_MAILBOX_ID);
1720                 }
1721             }
1722         }
1723     }
1724 
1725     /**
1726      * Note that there are two ways the EAS ExchangeService service can be created:
1727      *
1728      * 1) as a background service instantiated via startService (which happens on boot, when the
1729      * first EAS account is created, etc), in which case the service thread is spun up, mailboxes
1730      * sync, etc. and
1731      * 2) to execute an RPC call from the UI, in which case the background service will already be
1732      * running most of the time (unless we're creating a first EAS account)
1733      *
1734      * If the running background service detects that there are no EAS accounts (on boot, if none
1735      * were created, or afterward if the last remaining EAS account is deleted), it will call
1736      * stopSelf() to terminate operation.
1737      *
1738      * The goal is to ensure that the background service is running at all times when there is at
1739      * least one EAS account in existence
1740      *
1741      * Because there are edge cases in which our process can crash (typically, this has been seen
1742      * in UI crashes, ANR's, etc.), it's possible for the UI to start up again without the
1743      * background service having been started.  We explicitly try to start the service in Welcome
1744      * (to handle the case of the app having been reloaded).  We also start the service on any
1745      * startSync call (if it isn't already running)
1746      */
1747     @Override
onCreate()1748     public void onCreate() {
1749         Utility.runAsync(new Runnable() {
1750             @Override
1751             public void run() {
1752                 // Quick checks first, before getting the lock
1753                 if (sStartingUp) return;
1754                 synchronized (sSyncLock) {
1755                     alwaysLog("!!! EAS ExchangeService, onCreate");
1756                     // Try to start up properly; we might be coming back from a crash that the Email
1757                     // application isn't aware of.
1758                     startService(new Intent(EmailServiceProxy.EXCHANGE_INTENT));
1759                     if (sStop) {
1760                         return;
1761                     }
1762                 }
1763             }});
1764     }
1765 
1766     @Override
onStartCommand(Intent intent, int flags, int startId)1767     public int onStartCommand(Intent intent, int flags, int startId) {
1768         alwaysLog("!!! EAS ExchangeService, onStartCommand, startingUp = " + sStartingUp +
1769                 ", running = " + (INSTANCE != null));
1770         if (!sStartingUp && INSTANCE == null) {
1771             sStartingUp = true;
1772             Utility.runAsync(new Runnable() {
1773                 @Override
1774                 public void run() {
1775                     try {
1776                         synchronized (sSyncLock) {
1777                             // ExchangeService cannot start unless we can connect to AccountService
1778                             if (!new AccountServiceProxy(ExchangeService.this).test()) {
1779                                 alwaysLog("!!! Email application not found; stopping self");
1780                                 stopSelf();
1781                             }
1782                             if (sDeviceId == null) {
1783                                 try {
1784                                     String deviceId = getDeviceId(ExchangeService.this);
1785                                     if (deviceId != null) {
1786                                         sDeviceId = deviceId;
1787                                     }
1788                                 } catch (IOException e) {
1789                                 }
1790                                 if (sDeviceId == null) {
1791                                     alwaysLog("!!! deviceId unknown; stopping self and retrying");
1792                                     stopSelf();
1793                                     // Try to restart ourselves in a few seconds
1794                                     Utility.runAsync(new Runnable() {
1795                                         @Override
1796                                         public void run() {
1797                                             try {
1798                                                 Thread.sleep(5000);
1799                                             } catch (InterruptedException e) {
1800                                             }
1801                                             startService(new Intent(
1802                                                     EmailServiceProxy.EXCHANGE_INTENT));
1803                                         }});
1804                                     return;
1805                                 }
1806                             }
1807                             // Run the reconciler and clean up mismatched accounts - if we weren't
1808                             // running when accounts were deleted, it won't have been called.
1809                             runAccountReconcilerSync(ExchangeService.this);
1810                             // Update other services depending on final account configuration
1811                             maybeStartExchangeServiceThread();
1812                             if (sServiceThread == null) {
1813                                 log("!!! EAS ExchangeService, stopping self");
1814                                 stopSelf();
1815                             } else if (sStop) {
1816                                 // If we were trying to stop, attempt a restart in 5 secs
1817                                 setAlarm(EXCHANGE_SERVICE_MAILBOX_ID, 5*SECONDS);
1818                             }
1819                         }
1820                     } finally {
1821                         sStartingUp = false;
1822                     }
1823                 }});
1824         }
1825         return Service.START_STICKY;
1826     }
1827 
1828     @Override
onDestroy()1829     public void onDestroy() {
1830         log("!!! EAS ExchangeService, onDestroy");
1831         // Handle shutting down off the UI thread
1832         Utility.runAsync(new Runnable() {
1833             @Override
1834             public void run() {
1835                 // Quick checks first, before getting the lock
1836                 if (INSTANCE == null || sServiceThread == null) return;
1837                 synchronized(sSyncLock) {
1838                     // Stop the sync manager thread and return
1839                     if (sServiceThread != null) {
1840                         sStop = true;
1841                         sServiceThread.interrupt();
1842                     }
1843                 }
1844             }});
1845     }
1846 
maybeStartExchangeServiceThread()1847     void maybeStartExchangeServiceThread() {
1848         // Start our thread...
1849         // See if there are any EAS accounts; otherwise, just go away
1850         if (sServiceThread == null || !sServiceThread.isAlive()) {
1851             if (EmailContent.count(this, HostAuth.CONTENT_URI, WHERE_PROTOCOL_EAS, null) > 0) {
1852                 log(sServiceThread == null ? "Starting thread..." : "Restarting thread...");
1853                 sServiceThread = new Thread(this, "ExchangeService");
1854                 INSTANCE = this;
1855                 sServiceThread.start();
1856             }
1857         }
1858     }
1859 
1860     /**
1861      * Start up the ExchangeService service if it's not already running
1862      * This is a stopgap for cases in which ExchangeService died (due to a crash somewhere in
1863      * com.android.email) and hasn't been restarted. See the comment for onCreate for details
1864      */
checkExchangeServiceServiceRunning()1865     static void checkExchangeServiceServiceRunning() {
1866         ExchangeService exchangeService = INSTANCE;
1867         if (exchangeService == null) return;
1868         if (sServiceThread == null) {
1869             log("!!! checkExchangeServiceServiceRunning; starting service...");
1870             exchangeService.startService(new Intent(exchangeService, ExchangeService.class));
1871         }
1872     }
1873 
run()1874     public void run() {
1875         sStop = false;
1876         alwaysLog("ExchangeService thread running");
1877         // If we're really debugging, turn on all logging
1878         if (Eas.DEBUG) {
1879             Eas.USER_LOG = true;
1880             Eas.PARSER_LOG = true;
1881             Eas.FILE_LOG = true;
1882         }
1883 
1884         TempDirectory.setTempDirectory(this);
1885 
1886         // If we need to wait for the debugger, do so
1887         if (Eas.WAIT_DEBUG) {
1888             Debug.waitForDebugger();
1889         }
1890 
1891         // Synchronize here to prevent a shutdown from happening while we initialize our observers
1892         // and receivers
1893         synchronized (sSyncLock) {
1894             if (INSTANCE != null) {
1895                 mResolver = getContentResolver();
1896 
1897                 // Set up our observers; we need them to know when to start/stop various syncs based
1898                 // on the insert/delete/update of mailboxes and accounts
1899                 // We also observe synced messages to trigger upsyncs at the appropriate time
1900                 mAccountObserver = new AccountObserver(mHandler);
1901                 mResolver.registerContentObserver(Account.NOTIFIER_URI, true, mAccountObserver);
1902                 mMailboxObserver = new MailboxObserver(mHandler);
1903                 mResolver.registerContentObserver(Mailbox.CONTENT_URI, false, mMailboxObserver);
1904                 mSyncedMessageObserver = new SyncedMessageObserver(mHandler);
1905                 mResolver.registerContentObserver(Message.SYNCED_CONTENT_URI, true,
1906                         mSyncedMessageObserver);
1907 
1908                 // Set up receivers for connectivity and background data setting
1909                 mConnectivityReceiver = new ConnectivityReceiver();
1910                 registerReceiver(mConnectivityReceiver, new IntentFilter(
1911                         ConnectivityManager.CONNECTIVITY_ACTION));
1912 
1913                 mBackgroundDataSettingReceiver = new ConnectivityReceiver();
1914                 registerReceiver(mBackgroundDataSettingReceiver, new IntentFilter(
1915                         ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED));
1916                 // Save away the current background data setting; we'll keep track of it with the
1917                 // receiver we just registered
1918                 ConnectivityManager cm = (ConnectivityManager)getSystemService(
1919                         Context.CONNECTIVITY_SERVICE);
1920                 mBackgroundData = cm.getBackgroundDataSetting();
1921 
1922                 // Do any required work to clean up our Mailboxes (this serves to upgrade
1923                 // mailboxes that existed prior to EmailProvider database version 17)
1924                 MailboxUtilities.fixupUninitializedParentKeys(this, getEasAccountSelector());
1925             }
1926         }
1927 
1928         try {
1929             // Loop indefinitely until we're shut down
1930             while (!sStop) {
1931                 runAwake(EXTRA_MAILBOX_ID);
1932                 waitForConnectivity();
1933                 mNextWaitReason = null;
1934                 long nextWait = checkMailboxes();
1935                 try {
1936                     synchronized (this) {
1937                         if (!mKicked) {
1938                             if (nextWait < 0) {
1939                                 log("Negative wait? Setting to 1s");
1940                                 nextWait = 1*SECONDS;
1941                             }
1942                             if (nextWait > 10*SECONDS) {
1943                                 if (mNextWaitReason != null) {
1944                                     log("Next awake " + nextWait / 1000 + "s: " + mNextWaitReason);
1945                                 }
1946                                 runAsleep(EXTRA_MAILBOX_ID, nextWait + (3*SECONDS));
1947                             }
1948                             wait(nextWait);
1949                         }
1950                     }
1951                 } catch (InterruptedException e) {
1952                     // Needs to be caught, but causes no problem
1953                     log("ExchangeService interrupted");
1954                 } finally {
1955                     synchronized (this) {
1956                         if (mKicked) {
1957                             //log("Wait deferred due to kick");
1958                             mKicked = false;
1959                         }
1960                     }
1961                 }
1962             }
1963             log("Shutdown requested");
1964         } catch (ProviderUnavailableException pue) {
1965             // Shutdown cleanly in this case
1966             // NOTE: Sync adapters will also crash with this error, but that is already handled
1967             // in the adapters themselves, i.e. they return cleanly via done().  When the Email
1968             // process starts running again, the Exchange process will be started again in due
1969             // course, assuming there is at least one existing EAS account.
1970             Log.e(TAG, "EmailProvider unavailable; shutting down");
1971             // Ask for our service to be restarted; this should kick-start the Email process as well
1972             startService(new Intent(this, ExchangeService.class));
1973         } catch (RuntimeException e) {
1974             // Crash; this is a completely unexpected runtime error
1975             Log.e(TAG, "RuntimeException in ExchangeService", e);
1976             throw e;
1977         } finally {
1978             shutdown();
1979         }
1980     }
1981 
shutdown()1982     private void shutdown() {
1983         synchronized (sSyncLock) {
1984             // If INSTANCE is null, we've already been shut down
1985             if (INSTANCE != null) {
1986                 log("ExchangeService shutting down...");
1987 
1988                 // Stop our running syncs
1989                 stopServiceThreads();
1990 
1991                 // Stop receivers
1992                 if (mConnectivityReceiver != null) {
1993                     unregisterReceiver(mConnectivityReceiver);
1994                 }
1995                 if (mBackgroundDataSettingReceiver != null) {
1996                     unregisterReceiver(mBackgroundDataSettingReceiver);
1997                 }
1998 
1999                 // Unregister observers
2000                 ContentResolver resolver = getContentResolver();
2001                 if (mSyncedMessageObserver != null) {
2002                     resolver.unregisterContentObserver(mSyncedMessageObserver);
2003                     mSyncedMessageObserver = null;
2004                 }
2005                 if (mAccountObserver != null) {
2006                     resolver.unregisterContentObserver(mAccountObserver);
2007                     mAccountObserver = null;
2008                 }
2009                 if (mMailboxObserver != null) {
2010                     resolver.unregisterContentObserver(mMailboxObserver);
2011                     mMailboxObserver = null;
2012                 }
2013                 unregisterCalendarObservers();
2014 
2015                 // Clear pending alarms and associated Intents
2016                 clearAlarms();
2017 
2018                 // Release our wake lock, if we have one
2019                 synchronized (mWakeLocks) {
2020                     if (mWakeLock != null) {
2021                         mWakeLock.release();
2022                         mWakeLock = null;
2023                     }
2024                 }
2025 
2026                 INSTANCE = null;
2027                 sServiceThread = null;
2028                 sStop = false;
2029                 log("Goodbye");
2030             }
2031         }
2032     }
2033 
2034     /**
2035      * Release a mailbox from the service map and release its wake lock.
2036      * NOTE: This method MUST be called while holding sSyncLock!
2037      *
2038      * @param mailboxId the id of the mailbox to be released
2039      */
releaseMailbox(long mailboxId)2040     private void releaseMailbox(long mailboxId) {
2041         mServiceMap.remove(mailboxId);
2042         releaseWakeLock(mailboxId);
2043     }
2044 
2045     /**
2046      * Check whether an Outbox (referenced by a Cursor) has any messages that can be sent
2047      * @param c the cursor to an Outbox
2048      * @return true if there is mail to be sent
2049      */
hasSendableMessages(Cursor outboxCursor)2050     private boolean hasSendableMessages(Cursor outboxCursor) {
2051         Cursor c = mResolver.query(Message.CONTENT_URI, Message.ID_COLUMN_PROJECTION,
2052                 EasOutboxService.MAILBOX_KEY_AND_NOT_SEND_FAILED,
2053                 new String[] {Long.toString(outboxCursor.getLong(Mailbox.CONTENT_ID_COLUMN))},
2054                 null);
2055         try {
2056             while (c.moveToNext()) {
2057                 if (!Utility.hasUnloadedAttachments(this, c.getLong(Message.CONTENT_ID_COLUMN))) {
2058                     return true;
2059                 }
2060             }
2061         } finally {
2062             if (c != null) {
2063                 c.close();
2064             }
2065         }
2066         return false;
2067     }
2068 
2069     /**
2070      * Determine whether the account is allowed to sync automatically, as opposed to manually, based
2071      * on whether the "require manual sync when roaming" policy is in force and applicable
2072      * @param account the account
2073      * @return whether or not the account can sync automatically
2074      */
canAutoSync(Account account)2075     /*package*/ static boolean canAutoSync(Account account) {
2076         ExchangeService exchangeService = INSTANCE;
2077         if (exchangeService == null) {
2078             return false;
2079         }
2080         NetworkInfo networkInfo = exchangeService.mNetworkInfo;
2081 
2082         // Enforce manual sync only while roaming here
2083         long policyKey = account.mPolicyKey;
2084         // Quick exit from this check
2085         if ((policyKey != 0) && (networkInfo != null) &&
2086                 (ConnectivityManager.isNetworkTypeMobile(networkInfo.getType()))) {
2087             // We'll cache the Policy data here
2088             Policy policy = account.mPolicy;
2089             if (policy == null) {
2090                 policy = Policy.restorePolicyWithId(INSTANCE, policyKey);
2091                 account.mPolicy = policy;
2092             }
2093             if (policy != null && policy.mRequireManualSyncWhenRoaming && networkInfo.isRoaming()) {
2094                 return false;
2095             }
2096         }
2097         return true;
2098     }
2099 
2100     /**
2101      * Convenience method to determine whether Email sync is enabled for a given account
2102      * @param account the Account in question
2103      * @return whether Email sync is enabled
2104      */
canSyncEmail(android.accounts.Account account)2105     private boolean canSyncEmail(android.accounts.Account account) {
2106         return ContentResolver.getSyncAutomatically(account, EmailContent.AUTHORITY);
2107     }
2108 
2109     /**
2110      * Determine whether a mailbox of a given type in a given account can be synced automatically
2111      * by ExchangeService.  This is an increasingly complex determination, taking into account
2112      * security policies and user settings (both within the Email application and in the Settings
2113      * application)
2114      *
2115      * @param account the Account that the mailbox is in
2116      * @param type the type of the Mailbox
2117      * @return whether or not to start a sync
2118      */
isMailboxSyncable(Account account, int type)2119     private boolean isMailboxSyncable(Account account, int type) {
2120         // This 'if' statement performs checks to see whether or not a mailbox is a
2121         // candidate for syncing based on policies, user settings, & other restrictions
2122         if (type == Mailbox.TYPE_CONTACTS || type == Mailbox.TYPE_CALENDAR) {
2123             // Contacts/Calendar obey this setting from ContentResolver
2124             if (!ContentResolver.getMasterSyncAutomatically()) {
2125                 return false;
2126             }
2127             // Get the right authority for the mailbox
2128             String authority;
2129             if (type == Mailbox.TYPE_CONTACTS) {
2130                 authority = ContactsContract.AUTHORITY;
2131             } else {
2132                 authority = CalendarContract.AUTHORITY;
2133                 if (!mCalendarObservers.containsKey(account.mId)){
2134                     // Make sure we have an observer for this Calendar, as
2135                     // we need to be able to detect sync state changes, sigh
2136                     registerCalendarObserver(account);
2137                 }
2138             }
2139             // See if "sync automatically" is set; if not, punt
2140             if (!ContentResolver.getSyncAutomatically(account.mAmAccount, authority)) {
2141                 return false;
2142             // See if the calendar is enabled from the Calendar app UI; if not, punt
2143             } else if ((type == Mailbox.TYPE_CALENDAR) && !isCalendarEnabled(account.mId)) {
2144                 return false;
2145             }
2146         // Never automatically sync trash
2147         } else if (type == Mailbox.TYPE_TRASH) {
2148             return false;
2149         // For non-outbox mail, we do three checks:
2150         // 1) are we restricted by policy (i.e. manual sync only),
2151         // 2) has the user checked the "Sync Email" box in Account Settings, and
2152         // 3) does the user have the master "background data" box checked in Settings
2153         } else if (type != Mailbox.TYPE_OUTBOX &&
2154                 (!canAutoSync(account) || !canSyncEmail(account.mAmAccount) ||
2155                         !mBackgroundData)) {
2156             return false;
2157         }
2158         return true;
2159     }
2160 
checkMailboxes()2161     private long checkMailboxes () {
2162         // First, see if any running mailboxes have been deleted
2163         ArrayList<Long> deletedMailboxes = new ArrayList<Long>();
2164         synchronized (sSyncLock) {
2165             for (long mailboxId: mServiceMap.keySet()) {
2166                 Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId);
2167                 if (m == null) {
2168                     deletedMailboxes.add(mailboxId);
2169                 }
2170             }
2171             // If so, stop them or remove them from the map
2172             for (Long mailboxId: deletedMailboxes) {
2173                 AbstractSyncService svc = mServiceMap.get(mailboxId);
2174                 if (svc == null || svc.mThread == null) {
2175                     releaseMailbox(mailboxId);
2176                     continue;
2177                 } else {
2178                     boolean alive = svc.mThread.isAlive();
2179                     log("Deleted mailbox: " + svc.mMailboxName);
2180                     if (alive) {
2181                         stopManualSync(mailboxId);
2182                     } else {
2183                         log("Removing from serviceMap");
2184                         releaseMailbox(mailboxId);
2185                     }
2186                 }
2187             }
2188         }
2189 
2190         long nextWait = EXCHANGE_SERVICE_HEARTBEAT_TIME;
2191         long now = System.currentTimeMillis();
2192 
2193         // Start up threads that need it; use a query which finds eas mailboxes where the
2194         // the sync interval is not "never".  This is the set of mailboxes that we control
2195         if (mAccountObserver == null) {
2196             log("mAccountObserver null; service died??");
2197             return nextWait;
2198         }
2199 
2200         Cursor c = getContentResolver().query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
2201                 mAccountObserver.getSyncableEasMailboxWhere(), null, null);
2202         if (c == null) throw new ProviderUnavailableException();
2203         try {
2204             while (c.moveToNext()) {
2205                 long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN);
2206                 AbstractSyncService service = null;
2207                 synchronized (sSyncLock) {
2208                     service = mServiceMap.get(mailboxId);
2209                 }
2210                 if (service == null) {
2211                     // Get the cached account
2212                     Account account = getAccountById(c.getInt(Mailbox.CONTENT_ACCOUNT_KEY_COLUMN));
2213                     if (account == null) continue;
2214 
2215                     // We handle a few types of mailboxes specially
2216                     int mailboxType = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
2217                     if (!isMailboxSyncable(account, mailboxType)) {
2218                         continue;
2219                     }
2220 
2221                     // Check whether we're in a hold (temporary or permanent)
2222                     SyncError syncError = mSyncErrorMap.get(mailboxId);
2223                     if (syncError != null) {
2224                         // Nothing we can do about fatal errors
2225                         if (syncError.fatal) continue;
2226                         if (now < syncError.holdEndTime) {
2227                             // If release time is earlier than next wait time,
2228                             // move next wait time up to the release time
2229                             if (syncError.holdEndTime < now + nextWait) {
2230                                 nextWait = syncError.holdEndTime - now;
2231                                 mNextWaitReason = "Release hold";
2232                             }
2233                             continue;
2234                         } else {
2235                             // Keep the error around, but clear the end time
2236                             syncError.holdEndTime = 0;
2237                         }
2238                     }
2239 
2240                     // Otherwise, we use the sync interval
2241                     long syncInterval = c.getInt(Mailbox.CONTENT_SYNC_INTERVAL_COLUMN);
2242                     if (syncInterval == Mailbox.CHECK_INTERVAL_PUSH) {
2243                         Mailbox m = EmailContent.getContent(c, Mailbox.class);
2244                         requestSync(m, SYNC_PUSH, null);
2245                     } else if (mailboxType == Mailbox.TYPE_OUTBOX) {
2246                         if (hasSendableMessages(c)) {
2247                             Mailbox m = EmailContent.getContent(c, Mailbox.class);
2248                             startServiceThread(new EasOutboxService(this, m), m);
2249                         }
2250                     } else if (syncInterval > 0 && syncInterval <= ONE_DAY_MINUTES) {
2251                         long lastSync = c.getLong(Mailbox.CONTENT_SYNC_TIME_COLUMN);
2252                         long sinceLastSync = now - lastSync;
2253                         long toNextSync = syncInterval*MINUTES - sinceLastSync;
2254                         String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
2255                         if (toNextSync <= 0) {
2256                             Mailbox m = EmailContent.getContent(c, Mailbox.class);
2257                             requestSync(m, SYNC_SCHEDULED, null);
2258                         } else if (toNextSync < nextWait) {
2259                             nextWait = toNextSync;
2260                             if (Eas.USER_LOG) {
2261                                 log("Next sync for " + name + " in " + nextWait/1000 + "s");
2262                             }
2263                             mNextWaitReason = "Scheduled sync, " + name;
2264                         } else if (Eas.USER_LOG) {
2265                             log("Next sync for " + name + " in " + toNextSync/1000 + "s");
2266                         }
2267                     }
2268                 } else {
2269                     Thread thread = service.mThread;
2270                     // Look for threads that have died and remove them from the map
2271                     if (thread != null && !thread.isAlive()) {
2272                         if (Eas.USER_LOG) {
2273                             log("Dead thread, mailbox released: " +
2274                                     c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN));
2275                         }
2276                         releaseMailbox(mailboxId);
2277                         // Restart this if necessary
2278                         if (nextWait > 3*SECONDS) {
2279                             nextWait = 3*SECONDS;
2280                             mNextWaitReason = "Clean up dead thread(s)";
2281                         }
2282                     } else {
2283                         long requestTime = service.mRequestTime;
2284                         if (requestTime > 0) {
2285                             long timeToRequest = requestTime - now;
2286                             if (timeToRequest <= 0) {
2287                                 service.mRequestTime = 0;
2288                                 service.alarm();
2289                             } else if (requestTime > 0 && timeToRequest < nextWait) {
2290                                 if (timeToRequest < 11*MINUTES) {
2291                                     nextWait = timeToRequest < 250 ? 250 : timeToRequest;
2292                                     mNextWaitReason = "Sync data change";
2293                                 } else {
2294                                     log("Illegal timeToRequest: " + timeToRequest);
2295                                 }
2296                             }
2297                         }
2298                     }
2299                 }
2300             }
2301         } finally {
2302             c.close();
2303         }
2304         return nextWait;
2305     }
2306 
2307     static public void serviceRequest(long mailboxId, int reason) {
2308         serviceRequest(mailboxId, 5*SECONDS, reason);
2309     }
2310 
2311     /**
2312      * Return a boolean indicating whether the mailbox can be synced
2313      * @param m the mailbox
2314      * @return whether or not the mailbox can be synced
2315      */
2316     public static boolean isSyncable(Mailbox m) {
2317         return m.loadsFromServer(HostAuth.SCHEME_EAS);
2318     }
2319 
2320     static public void serviceRequest(long mailboxId, long ms, int reason) {
2321         ExchangeService exchangeService = INSTANCE;
2322         if (exchangeService == null) return;
2323         Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
2324         if (m == null || !isSyncable(m)) return;
2325         try {
2326             AbstractSyncService service = exchangeService.mServiceMap.get(mailboxId);
2327             if (service != null) {
2328                 service.mRequestTime = System.currentTimeMillis() + ms;
2329                 kick("service request");
2330             } else {
2331                 startManualSync(mailboxId, reason, null);
2332             }
2333         } catch (Exception e) {
2334             e.printStackTrace();
2335         }
2336     }
2337 
2338     static public void serviceRequestImmediate(long mailboxId) {
2339         ExchangeService exchangeService = INSTANCE;
2340         if (exchangeService == null) return;
2341         AbstractSyncService service = exchangeService.mServiceMap.get(mailboxId);
2342         if (service != null) {
2343             service.mRequestTime = System.currentTimeMillis();
2344             Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
2345             if (m != null) {
2346                 service.mAccount = Account.restoreAccountWithId(exchangeService, m.mAccountKey);
2347                 service.mMailbox = m;
2348                 kick("service request immediate");
2349             }
2350         }
2351     }
2352 
2353     static public void sendMessageRequest(Request req) {
2354         ExchangeService exchangeService = INSTANCE;
2355         if (exchangeService == null) return;
2356         Message msg = Message.restoreMessageWithId(exchangeService, req.mMessageId);
2357         if (msg == null) {
2358             return;
2359         }
2360         long mailboxId = msg.mMailboxKey;
2361         AbstractSyncService service = exchangeService.mServiceMap.get(mailboxId);
2362 
2363         if (service == null) {
2364             startManualSync(mailboxId, SYNC_SERVICE_PART_REQUEST, req);
2365             kick("part request");
2366         } else {
2367             service.addRequest(req);
2368         }
2369     }
2370 
2371     /**
2372      * Determine whether a given Mailbox can be synced, i.e. is not already syncing and is not in
2373      * an error state
2374      *
2375      * @param mailboxId
2376      * @return whether or not the Mailbox is available for syncing (i.e. is a valid push target)
2377      */
2378     static public int pingStatus(long mailboxId) {
2379         ExchangeService exchangeService = INSTANCE;
2380         if (exchangeService == null) return PING_STATUS_OK;
2381         // Already syncing...
2382         if (exchangeService.mServiceMap.get(mailboxId) != null) {
2383             return PING_STATUS_RUNNING;
2384         }
2385         // No errors or a transient error, don't ping...
2386         SyncError error = exchangeService.mSyncErrorMap.get(mailboxId);
2387         if (error != null) {
2388             if (error.fatal) {
2389                 return PING_STATUS_UNABLE;
2390             } else if (error.holdEndTime > 0) {
2391                 return PING_STATUS_WAITING;
2392             }
2393         }
2394         return PING_STATUS_OK;
2395     }
2396 
startManualSync(long mailboxId, int reason, Request req)2397     static public void startManualSync(long mailboxId, int reason, Request req) {
2398         ExchangeService exchangeService = INSTANCE;
2399         if (exchangeService == null) return;
2400         synchronized (sSyncLock) {
2401             AbstractSyncService svc = exchangeService.mServiceMap.get(mailboxId);
2402             if (svc == null) {
2403                 exchangeService.mSyncErrorMap.remove(mailboxId);
2404                 Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
2405                 if (m != null) {
2406                     log("Starting sync for " + m.mDisplayName);
2407                     exchangeService.requestSync(m, reason, req);
2408                 }
2409             } else {
2410                 // If this is a ui request, set the sync reason for the service
2411                 if (reason >= SYNC_CALLBACK_START) {
2412                     svc.mSyncReason = reason;
2413                 }
2414             }
2415         }
2416     }
2417 
2418     // DO NOT CALL THIS IN A LOOP ON THE SERVICEMAP
stopManualSync(long mailboxId)2419     static public void stopManualSync(long mailboxId) {
2420         ExchangeService exchangeService = INSTANCE;
2421         if (exchangeService == null) return;
2422         synchronized (sSyncLock) {
2423             AbstractSyncService svc = exchangeService.mServiceMap.get(mailboxId);
2424             if (svc != null) {
2425                 log("Stopping sync for " + svc.mMailboxName);
2426                 svc.stop();
2427                 svc.mThread.interrupt();
2428                 exchangeService.releaseWakeLock(mailboxId);
2429             }
2430         }
2431     }
2432 
2433     /**
2434      * Wake up ExchangeService to check for mailboxes needing service
2435      */
kick(String reason)2436     static public void kick(String reason) {
2437        ExchangeService exchangeService = INSTANCE;
2438        if (exchangeService != null) {
2439             synchronized (exchangeService) {
2440                 //INSTANCE.log("Kick: " + reason);
2441                 exchangeService.mKicked = true;
2442                 exchangeService.notify();
2443             }
2444         }
2445         if (sConnectivityLock != null) {
2446             synchronized (sConnectivityLock) {
2447                 sConnectivityLock.notify();
2448             }
2449         }
2450     }
2451 
accountUpdated(long acctId)2452     static public void accountUpdated(long acctId) {
2453         ExchangeService exchangeService = INSTANCE;
2454         if (exchangeService == null) return;
2455         synchronized (sSyncLock) {
2456             for (AbstractSyncService svc : exchangeService.mServiceMap.values()) {
2457                 if (svc.mAccount.mId == acctId) {
2458                     svc.mAccount = Account.restoreAccountWithId(exchangeService, acctId);
2459                 }
2460             }
2461         }
2462     }
2463 
2464     /**
2465      * Tell ExchangeService to remove the mailbox from the map of mailboxes with sync errors
2466      * @param mailboxId the id of the mailbox
2467      */
removeFromSyncErrorMap(long mailboxId)2468     static public void removeFromSyncErrorMap(long mailboxId) {
2469         ExchangeService exchangeService = INSTANCE;
2470         if (exchangeService != null) {
2471             exchangeService.mSyncErrorMap.remove(mailboxId);
2472         }
2473     }
2474 
isRunningInServiceThread(long mailboxId)2475     private boolean isRunningInServiceThread(long mailboxId) {
2476         AbstractSyncService syncService = mServiceMap.get(mailboxId);
2477         Thread thisThread = Thread.currentThread();
2478         return syncService != null && syncService.mThread != null &&
2479             thisThread == syncService.mThread;
2480     }
2481 
2482     /**
2483      * Sent by services indicating that their thread is finished; action depends on the exitStatus
2484      * of the service.
2485      *
2486      * @param svc the service that is finished
2487      */
done(AbstractSyncService svc)2488     static public void done(AbstractSyncService svc) {
2489         ExchangeService exchangeService = INSTANCE;
2490         if (exchangeService == null) return;
2491         synchronized(sSyncLock) {
2492             long mailboxId = svc.mMailboxId;
2493             // If we're no longer the syncing thread for the mailbox, just return
2494             if (!exchangeService.isRunningInServiceThread(mailboxId)) {
2495                 return;
2496             }
2497             exchangeService.releaseMailbox(mailboxId);
2498 
2499             ConcurrentHashMap<Long, SyncError> errorMap = exchangeService.mSyncErrorMap;
2500             SyncError syncError = errorMap.get(mailboxId);
2501 
2502             int exitStatus = svc.mExitStatus;
2503             Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
2504             if (m == null) return;
2505 
2506             if (exitStatus != AbstractSyncService.EXIT_LOGIN_FAILURE) {
2507                 long accountId = m.mAccountKey;
2508                 Account account = Account.restoreAccountWithId(exchangeService, accountId);
2509                 if (account == null) return;
2510                 if (exchangeService.releaseSyncHolds(exchangeService,
2511                         AbstractSyncService.EXIT_LOGIN_FAILURE, account)) {
2512                     new AccountServiceProxy(exchangeService).notifyLoginSucceeded(accountId);
2513                 }
2514             }
2515 
2516             switch (exitStatus) {
2517                 case AbstractSyncService.EXIT_DONE:
2518                     if (svc.hasPendingRequests()) {
2519                         // TODO Handle this case
2520                     }
2521                     errorMap.remove(mailboxId);
2522                     // If we've had a successful sync, clear the shutdown count
2523                     synchronized (ExchangeService.class) {
2524                         sClientConnectionManagerShutdownCount = 0;
2525                     }
2526                     break;
2527                 // I/O errors get retried at increasing intervals
2528                 case AbstractSyncService.EXIT_IO_ERROR:
2529                     if (syncError != null) {
2530                         syncError.escalate();
2531                         log(m.mDisplayName + " held for " + syncError.holdDelay + "ms");
2532                     } else {
2533                         errorMap.put(mailboxId, exchangeService.new SyncError(exitStatus, false));
2534                         log(m.mDisplayName + " added to syncErrorMap, hold for 15s");
2535                     }
2536                     break;
2537                 // These errors are not retried automatically
2538                 case AbstractSyncService.EXIT_LOGIN_FAILURE:
2539                     new AccountServiceProxy(exchangeService).notifyLoginFailed(m.mAccountKey);
2540                     // Fall through
2541                 case AbstractSyncService.EXIT_SECURITY_FAILURE:
2542                 case AbstractSyncService.EXIT_ACCESS_DENIED:
2543                 case AbstractSyncService.EXIT_EXCEPTION:
2544                     errorMap.put(mailboxId, exchangeService.new SyncError(exitStatus, true));
2545                     break;
2546             }
2547             kick("sync completed");
2548         }
2549     }
2550 
2551     /**
2552      * Given the status string from a Mailbox, return the type code for the last sync
2553      * @param status the syncStatus column of a Mailbox
2554      * @return
2555      */
getStatusType(String status)2556     static public int getStatusType(String status) {
2557         if (status == null) {
2558             return -1;
2559         } else {
2560             return status.charAt(STATUS_TYPE_CHAR) - '0';
2561         }
2562     }
2563 
2564     /**
2565      * Given the status string from a Mailbox, return the change count for the last sync
2566      * The change count is the number of adds + deletes + changes in the last sync
2567      * @param status the syncStatus column of a Mailbox
2568      * @return
2569      */
getStatusChangeCount(String status)2570     static public int getStatusChangeCount(String status) {
2571         try {
2572             String s = status.substring(STATUS_CHANGE_COUNT_OFFSET);
2573             return Integer.parseInt(s);
2574         } catch (RuntimeException e) {
2575             return -1;
2576         }
2577     }
2578 
getContext()2579     static public Context getContext() {
2580         return INSTANCE;
2581     }
2582 }
2583