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