Home | History | Annotate | Download | only in exchange
      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.content.ContentResolver;
     21 import android.content.ContentUris;
     22 import android.content.ContentValues;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.database.ContentObserver;
     26 import android.database.Cursor;
     27 import android.net.Uri;
     28 import android.os.Bundle;
     29 import android.os.Handler;
     30 import android.os.IBinder;
     31 import android.os.RemoteException;
     32 import android.provider.CalendarContract;
     33 import android.provider.CalendarContract.Calendars;
     34 import android.provider.CalendarContract.Events;
     35 
     36 import com.android.emailcommon.provider.Account;
     37 import com.android.emailcommon.provider.EmailContent.Attachment;
     38 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
     39 import com.android.emailcommon.provider.EmailContent.Message;
     40 import com.android.emailcommon.provider.HostAuth;
     41 import com.android.emailcommon.provider.Mailbox;
     42 import com.android.emailcommon.provider.MailboxUtilities;
     43 import com.android.emailcommon.provider.ProviderUnavailableException;
     44 import com.android.emailcommon.service.AccountServiceProxy;
     45 import com.android.emailcommon.service.IEmailService;
     46 import com.android.emailcommon.service.IEmailServiceCallback;
     47 import com.android.emailcommon.service.IEmailServiceCallback.Stub;
     48 import com.android.emailcommon.service.SearchParams;
     49 import com.android.emailsync.AbstractSyncService;
     50 import com.android.emailsync.PartRequest;
     51 import com.android.emailsync.SyncManager;
     52 import com.android.exchange.eas.EasSearch;
     53 import com.android.exchange.utility.FileLogger;
     54 import com.android.mail.providers.UIProvider.AccountCapabilities;
     55 import com.android.mail.utils.LogUtils;
     56 
     57 import java.util.concurrent.ConcurrentHashMap;
     58 
     59 /**
     60  * The ExchangeService handles all aspects of starting, maintaining, and stopping the various sync
     61  * adapters used by Exchange.  However, it is capable of handing any kind of email sync, and it
     62  * would be appropriate to use for IMAP push, when that functionality is added to the Email
     63  * application.
     64  *
     65  * The Email application communicates with EAS sync adapters via ExchangeService's binder interface,
     66  * which exposes UI-related functionality to the application (see the definitions below)
     67  *
     68  * ExchangeService uses ContentObservers to detect changes to accounts, mailboxes, and messages in
     69  * order to maintain proper 2-way syncing of data.  (More documentation to follow)
     70  *
     71  */
     72 public class ExchangeService extends SyncManager {
     73 
     74     private static final String TAG = Eas.LOG_TAG;
     75 
     76     private static final String WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX =
     77         MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.TYPE + "!=" +
     78         Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + " and " + MailboxColumns.SYNC_INTERVAL +
     79         " IN (" + Mailbox.CHECK_INTERVAL_PING + ',' + Mailbox.CHECK_INTERVAL_PUSH + ')';
     80     private static final String WHERE_MAILBOX_KEY = Message.MAILBOX_KEY + "=?";
     81     private static final String WHERE_CALENDAR_ID = Events.CALENDAR_ID + "=?";
     82     private static final String ACCOUNT_KEY_IN = MailboxColumns.ACCOUNT_KEY + " in (";
     83 
     84     // Offsets into the syncStatus data for EAS that indicate type, exit status, and change count
     85     // The format is S<type_char>:<exit_char>:<change_count>
     86     public static final int STATUS_TYPE_CHAR = 1;
     87     public static final int STATUS_EXIT_CHAR = 3;
     88     public static final int STATUS_CHANGE_COUNT_OFFSET = 5;
     89 
     90     private static final int EAS_12_CAPABILITIES =
     91             AccountCapabilities.SYNCABLE_FOLDERS |
     92             AccountCapabilities.SERVER_SEARCH |
     93             AccountCapabilities.FOLDER_SERVER_SEARCH |
     94             AccountCapabilities.SMART_REPLY |
     95             AccountCapabilities.SERVER_SEARCH |
     96             AccountCapabilities.UNDO;
     97 
     98     private static final int EAS_2_CAPABILITIES =
     99             AccountCapabilities.SYNCABLE_FOLDERS |
    100             AccountCapabilities.SMART_REPLY |
    101             AccountCapabilities.UNDO;
    102 
    103     // We synchronize on this for all actions affecting the service and error maps
    104     private static final Object sSyncLock = new Object();
    105     private String mEasAccountSelector;
    106 
    107     // Concurrent because CalendarSyncAdapter can modify the map during a wipe
    108     private final ConcurrentHashMap<Long, CalendarObserver> mCalendarObservers =
    109             new ConcurrentHashMap<Long, CalendarObserver>();
    110 
    111     private final Intent mIntent = new Intent(Eas.EXCHANGE_SERVICE_INTENT_ACTION);
    112 
    113     /**
    114      * Create our EmailService implementation here.
    115      */
    116     private final IEmailService.Stub mBinder = new IEmailService.Stub() {
    117 
    118         @Override
    119         public Bundle validate(HostAuth hostAuth) throws RemoteException {
    120             return AbstractSyncService.validate(EasSyncService.class,
    121                     hostAuth, ExchangeService.this);
    122         }
    123 
    124         @Override
    125         public Bundle autoDiscover(String userName, String password) throws RemoteException {
    126             HostAuth hostAuth = new HostAuth();
    127             hostAuth.mLogin = userName;
    128             hostAuth.mPassword = password;
    129             hostAuth.mFlags = HostAuth.FLAG_AUTHENTICATE | HostAuth.FLAG_SSL;
    130             hostAuth.mPort = 443;
    131             return new EasSyncService().tryAutodiscover(ExchangeService.this, hostAuth);
    132         }
    133 
    134         @Override
    135         public void loadAttachment(final IEmailServiceCallback callback, final long accountId,
    136                 final long attachmentId, final boolean background) throws RemoteException {
    137             Attachment att = Attachment.restoreAttachmentWithId(ExchangeService.this, attachmentId);
    138             log("loadAttachment " + attachmentId + ": " + att.mFileName);
    139             sendMessageRequest(new PartRequest(att, null, null));
    140         }
    141 
    142         @Override
    143         public void updateFolderList(long accountId) throws RemoteException {
    144             reloadFolderList(ExchangeService.this, accountId, false);
    145         }
    146 
    147         @Override
    148         public void setLogging(int flags) throws RemoteException {
    149             // Protocol logging
    150             Eas.setUserDebug(flags);
    151             // Sync logging
    152             setUserDebug(flags);
    153         }
    154 
    155         @Override
    156         public void sendMeetingResponse(long messageId, int response) throws RemoteException {
    157             sendMessageRequest(new MeetingResponseRequest(messageId, response));
    158         }
    159 
    160         /**
    161          * Delete PIM (calendar, contacts) data for the specified account
    162          *
    163          * @param emailAddress the email address for the account whose data should be deleted
    164          * @throws RemoteException
    165          */
    166         @Override
    167         public void deleteAccountPIMData(final String emailAddress) throws RemoteException {
    168             // ExchangeService is deprecated so I am deleting rather than fixing this function.
    169         }
    170 
    171         @Override
    172         public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) {
    173             SyncManager exchangeService = INSTANCE;
    174             if (exchangeService == null) return 0;
    175             EasSearch op = new EasSearch(exchangeService, accountId, searchParams, destMailboxId);
    176             op.performOperation();
    177             return op.getTotalResults();
    178         }
    179 
    180         @Override
    181         public void sendMail(long accountId) throws RemoteException {}
    182 
    183         @Override
    184         public void pushModify(long accountId) throws RemoteException {}
    185 
    186         @Override
    187         public void sync(final long accountId, final boolean updateFolderList,
    188                 final int mailboxType, final long[] folders) {}
    189     };
    190 
    191     /**
    192      * Return a list of all Accounts in EmailProvider.  Because the result of this call may be used
    193      * in account reconciliation, an exception is thrown if the result cannot be guaranteed accurate
    194      * @param context the caller's context
    195      * @param accounts a list that Accounts will be added into
    196      * @return the list of Accounts
    197      * @throws ProviderUnavailableException if the list of Accounts cannot be guaranteed valid
    198      */
    199     @Override
    200     public AccountList collectAccounts(Context context, AccountList accounts) {
    201         ContentResolver resolver = context.getContentResolver();
    202         Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null,
    203                 null);
    204         // We must throw here; callers might use the information we provide for reconciliation, etc.
    205         if (c == null) throw new ProviderUnavailableException();
    206         try {
    207             ContentValues cv = new ContentValues();
    208             while (c.moveToNext()) {
    209                 long hostAuthId = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN);
    210                 if (hostAuthId > 0) {
    211                     HostAuth ha = HostAuth.restoreHostAuthWithId(context, hostAuthId);
    212                     if (ha != null && ha.mProtocol.equals(Eas.PROTOCOL)) {
    213                         Account account = new Account();
    214                         account.restore(c);
    215                         // Cache the HostAuth
    216                         account.mHostAuthRecv = ha;
    217                         accounts.add(account);
    218                         // Fixup flags for inbox (should accept moved mail)
    219                         Mailbox inbox = Mailbox.restoreMailboxOfType(context, account.mId,
    220                                 Mailbox.TYPE_INBOX);
    221                         if (inbox != null &&
    222                                 ((inbox.mFlags & Mailbox.FLAG_ACCEPTS_MOVED_MAIL) == 0)) {
    223                             cv.put(MailboxColumns.FLAGS,
    224                                     inbox.mFlags | Mailbox.FLAG_ACCEPTS_MOVED_MAIL);
    225                             resolver.update(
    226                                     ContentUris.withAppendedId(Mailbox.CONTENT_URI, inbox.mId), cv,
    227                                     null, null);
    228                         }
    229                     }
    230                 }
    231             }
    232         } finally {
    233             c.close();
    234         }
    235         return accounts;
    236     }
    237 
    238     public static boolean onSecurityHold(Account account) {
    239         return (account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0;
    240     }
    241 
    242     private static boolean onSyncDisabledHold(Account account) {
    243         return (account.mFlags & Account.FLAGS_SYNC_DISABLED) != 0;
    244     }
    245 
    246     private static Uri eventsAsSyncAdapter(final Uri uri, final String account,
    247             final String accountType) {
    248         return uri.buildUpon()
    249                 .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
    250                 .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
    251                 .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
    252     }
    253 
    254     /**
    255      * Unregister all CalendarObserver's
    256      */
    257     static public void unregisterCalendarObservers() {
    258         ExchangeService exchangeService = (ExchangeService)INSTANCE;
    259         if (exchangeService == null) return;
    260         ContentResolver resolver = exchangeService.mResolver;
    261         for (CalendarObserver observer: exchangeService.mCalendarObservers.values()) {
    262             resolver.unregisterContentObserver(observer);
    263         }
    264         exchangeService.mCalendarObservers.clear();
    265     }
    266 
    267     private class CalendarObserver extends ContentObserver {
    268         long mAccountId;
    269         long mCalendarId;
    270         long mSyncEvents;
    271         String mAccountName;
    272 
    273         public CalendarObserver(Handler handler, Account account) {
    274             super(handler);
    275             mAccountId = account.mId;
    276             mAccountName = account.mEmailAddress;
    277 
    278             // Find the Calendar for this account
    279             Cursor c = mResolver.query(Calendars.CONTENT_URI,
    280                     new String[] {Calendars._ID, Calendars.SYNC_EVENTS},
    281                     CALENDAR_SELECTION,
    282                     new String[] {account.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE},
    283                     null);
    284             if (c != null) {
    285                 // Save its id and its sync events status
    286                 try {
    287                     if (c.moveToFirst()) {
    288                         mCalendarId = c.getLong(0);
    289                         mSyncEvents = c.getLong(1);
    290                     }
    291                 } finally {
    292                     c.close();
    293                 }
    294             }
    295         }
    296 
    297         private void onChangeInBackground() {
    298             try {
    299                 Cursor c = mResolver.query(Calendars.CONTENT_URI,
    300                         new String[] {Calendars.SYNC_EVENTS}, Calendars._ID + "=?",
    301                         new String[] {Long.toString(mCalendarId)}, null);
    302                 if (c == null) return;
    303                 // Get its sync events; if it's changed, we've got work to do
    304                 try {
    305                     if (c.moveToFirst()) {
    306                         long newSyncEvents = c.getLong(0);
    307                         if (newSyncEvents != mSyncEvents) {
    308                             log("_sync_events changed for calendar in " + mAccountName);
    309                             Mailbox mailbox = Mailbox.restoreMailboxOfType(INSTANCE,
    310                                     mAccountId, Mailbox.TYPE_CALENDAR);
    311                             // Sanity check for mailbox deletion
    312                             if (mailbox == null) return;
    313                             ContentValues cv = new ContentValues();
    314                             if (newSyncEvents == 0) {
    315                                 // When sync is disabled, we're supposed to delete
    316                                 // all events in the calendar
    317                                 log("Deleting events and setting syncKey to 0 for " +
    318                                         mAccountName);
    319                                 // First, stop any sync that's ongoing
    320                                 stopManualSync(mailbox.mId);
    321                                 // Set the syncKey to 0 (reset)
    322                                 EasSyncService service =
    323                                     EasSyncService.getServiceForMailbox(
    324                                             INSTANCE, mailbox);
    325 
    326                                 // CalendarSyncAdapter is gone, and this class is deprecated.
    327                                 // Just leaving this commented out code here for reference:
    328                                 // Reset the sync key locally and stop syncing
    329 //                                CalendarSyncAdapter adapter =
    330 //                                    new CalendarSyncAdapter(service);
    331 //                                try {
    332 //                                    adapter.setSyncKey("0", false);
    333 //                                } catch (IOException e) {
    334 //                                    // The provider can't be reached; nothing to be done
    335 //                                }
    336 
    337                                 cv.put(Mailbox.SYNC_KEY, "0");
    338                                 cv.put(Mailbox.SYNC_INTERVAL,
    339                                         Mailbox.CHECK_INTERVAL_NEVER);
    340                                 mResolver.update(ContentUris.withAppendedId(
    341                                         Mailbox.CONTENT_URI, mailbox.mId), cv, null,
    342                                         null);
    343                                 // Delete all events using the sync adapter
    344                                 // parameter so that the deletion is only local
    345                                 Uri eventsAsSyncAdapter = eventsAsSyncAdapter(Events.CONTENT_URI,
    346                                         mAccountName, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
    347                                 mResolver.delete(eventsAsSyncAdapter, WHERE_CALENDAR_ID,
    348                                         new String[] {Long.toString(mCalendarId)});
    349                             } else {
    350                                 // Make this a push mailbox and kick; this will start
    351                                 // a resync of the Calendar; the account mailbox will
    352                                 // ping on this during the next cycle of the ping loop
    353                                 cv.put(Mailbox.SYNC_INTERVAL,
    354                                         Mailbox.CHECK_INTERVAL_PUSH);
    355                                 mResolver.update(ContentUris.withAppendedId(
    356                                         Mailbox.CONTENT_URI, mailbox.mId), cv, null,
    357                                         null);
    358                                 kick("calendar sync changed");
    359                             }
    360 
    361                             // Save away the new value
    362                             mSyncEvents = newSyncEvents;
    363                         }
    364                     }
    365                 } finally {
    366                     c.close();
    367                 }
    368             } catch (ProviderUnavailableException e) {
    369                 LogUtils.w(TAG, "Observer failed; provider unavailable");
    370             }
    371         }
    372 
    373 
    374         @Override
    375         public synchronized void onChange(boolean selfChange) {
    376             // See if the user has changed syncing of our calendar
    377             if (!selfChange) {
    378                 new Thread(new Runnable() {
    379                     @Override
    380                     public void run() {
    381                         onChangeInBackground();
    382                     }
    383                 }, "Calendar Observer").start();
    384             }
    385         }
    386     }
    387 
    388     /**
    389      * Blocking call to the account reconciler
    390      */
    391     @Override
    392     public void runAccountReconcilerSync(Context context) {
    393         alwaysLog("Reconciling accounts...");
    394         new AccountServiceProxy(context).reconcileAccounts(
    395                 Eas.PROTOCOL, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
    396     }
    397 
    398     public static void log(String str) {
    399         log(TAG, str);
    400     }
    401 
    402     public static void log(String tag, String str) {
    403         if (Eas.USER_LOG) {
    404             LogUtils.d(tag, str);
    405             if (Eas.FILE_LOG) {
    406                 FileLogger.log(tag, str);
    407             }
    408         }
    409     }
    410 
    411     public static void alwaysLog(String str) {
    412         if (!Eas.USER_LOG) {
    413             LogUtils.d(TAG, str);
    414         } else {
    415             log(str);
    416         }
    417     }
    418 
    419     /**
    420      * EAS requires a unique device id, so that sync is possible from a variety of different
    421      * devices (e.g. the syncKey is specific to a device)  If we're on an emulator or some other
    422      * device that doesn't provide one, we can create it as "device".
    423      * This would work on a real device as well, but it would be better to use the "real" id if
    424      * it's available
    425      */
    426     static public String getDeviceId(Context context) {
    427         if (sDeviceId == null) {
    428             sDeviceId = new AccountServiceProxy(context).getDeviceId();
    429             alwaysLog("Received deviceId from Email app: " + sDeviceId);
    430         }
    431         return sDeviceId;
    432     }
    433 
    434     @Override
    435     public IBinder onBind(Intent arg0) {
    436         return mBinder;
    437     }
    438 
    439     static private void reloadFolderListFailed(long accountId) {
    440 
    441     }
    442 
    443     static public void reloadFolderList(Context context, long accountId, boolean force) {
    444         SyncManager exchangeService = INSTANCE;
    445         if (exchangeService == null) return;
    446         Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI,
    447                 Mailbox.CONTENT_PROJECTION, MailboxColumns.ACCOUNT_KEY + "=? AND " +
    448                 MailboxColumns.TYPE + "=?",
    449                 new String[] {Long.toString(accountId),
    450                     Long.toString(Mailbox.TYPE_EAS_ACCOUNT_MAILBOX)}, null);
    451         try {
    452             if (c.moveToFirst()) {
    453                 synchronized(sSyncLock) {
    454                     Mailbox mailbox = new Mailbox();
    455                     mailbox.restore(c);
    456                     Account acct = Account.restoreAccountWithId(context, accountId);
    457                     if (acct == null) {
    458                         reloadFolderListFailed(accountId);
    459                         return;
    460                     }
    461                     String syncKey = acct.mSyncKey;
    462                     // No need to reload the list if we don't have one
    463                     if (!force && (syncKey == null || syncKey.equals("0"))) {
    464                         reloadFolderListFailed(accountId);
    465                         return;
    466                     }
    467 
    468                     // Change all ping/push boxes to push/hold
    469                     ContentValues cv = new ContentValues();
    470                     cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH_HOLD);
    471                     context.getContentResolver().update(Mailbox.CONTENT_URI, cv,
    472                             WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX,
    473                             new String[] {Long.toString(accountId)});
    474                     log("Set push/ping boxes to push/hold");
    475 
    476                     long id = mailbox.mId;
    477                     AbstractSyncService svc = exchangeService.mServiceMap.get(id);
    478                     // Tell the service we're done
    479                     if (svc != null) {
    480                         synchronized (svc.getSynchronizer()) {
    481                             svc.stop();
    482                             // Interrupt the thread so that it can stop
    483                             Thread thread = svc.mThread;
    484                             if (thread != null) {
    485                                 thread.setName(thread.getName() + " (Stopped)");
    486                                 thread.interrupt();
    487                             }
    488                         }
    489                         // Abandon the service
    490                         exchangeService.releaseMailbox(id);
    491                         // And have it start naturally
    492                         kick("reload folder list");
    493                     }
    494                 }
    495             }
    496         } finally {
    497             c.close();
    498         }
    499     }
    500 
    501     /**
    502      * Informs ExchangeService that an account has a new folder list; as a result, any existing
    503      * folder might have become invalid.  Therefore, we act as if the account has been deleted, and
    504      * then we reinitialize it.
    505      *
    506      * @param acctId
    507      */
    508     static public void stopNonAccountMailboxSyncsForAccount(long acctId) {
    509         SyncManager exchangeService = INSTANCE;
    510         if (exchangeService != null) {
    511             exchangeService.stopAccountSyncs(acctId, false);
    512             kick("reload folder list");
    513         }
    514     }
    515 
    516     /**
    517      * Start up the ExchangeService service if it's not already running
    518      * This is a stopgap for cases in which ExchangeService died (due to a crash somewhere in
    519      * com.android.email) and hasn't been restarted. See the comment for onCreate for details
    520      */
    521     static void checkExchangeServiceServiceRunning() {
    522         SyncManager exchangeService = INSTANCE;
    523         if (exchangeService == null) return;
    524         if (sServiceThread == null) {
    525             log("!!! checkExchangeServiceServiceRunning; starting service...");
    526             exchangeService.startService(new Intent(exchangeService, ExchangeService.class));
    527         }
    528     }
    529 
    530     @Override
    531     public AccountObserver getAccountObserver(
    532             Handler handler) {
    533         return new AccountObserver(handler) {
    534             @Override
    535             public void newAccount(long acctId) {
    536                 Account acct = Account.restoreAccountWithId(getContext(), acctId);
    537                 if (acct == null) {
    538                     // This account is in a bad state; don't create the mailbox.
    539                     LogUtils.e(TAG, "Cannot initialize bad acctId: " + acctId);
    540                     return;
    541                 }
    542                 Mailbox main = new Mailbox();
    543                 main.mDisplayName = Eas.ACCOUNT_MAILBOX_PREFIX;
    544                 main.mServerId = Eas.ACCOUNT_MAILBOX_PREFIX + System.nanoTime();
    545                 main.mAccountKey = acct.mId;
    546                 main.mType = Mailbox.TYPE_EAS_ACCOUNT_MAILBOX;
    547                 main.mSyncInterval = Mailbox.CHECK_INTERVAL_PUSH;
    548                 main.mFlagVisible = false;
    549                 main.save(getContext());
    550                 log("Initializing account: " + acct.mDisplayName);
    551             }
    552         };
    553     }
    554 
    555     @Override
    556     public void onStartup() {
    557         // Do any required work to clean up our Mailboxes (this serves to upgrade
    558         // mailboxes that existed prior to EmailProvider database version 17)
    559         MailboxUtilities.fixupUninitializedParentKeys(this, getAccountsSelector());
    560     }
    561 
    562     @Override
    563     public AbstractSyncService getServiceForMailbox(Context context,
    564             Mailbox m) {
    565         switch(m.mType) {
    566             case Mailbox.TYPE_EAS_ACCOUNT_MAILBOX:
    567                 return new EasAccountService(context, m);
    568             case Mailbox.TYPE_OUTBOX:
    569                 return new EasOutboxService(context, m);
    570             default:
    571                 return new EasSyncService(context, m);
    572         }
    573     }
    574 
    575     @Override
    576     public String getAccountsSelector() {
    577         if (mEasAccountSelector == null) {
    578             StringBuilder sb = new StringBuilder(ACCOUNT_KEY_IN);
    579             boolean first = true;
    580             synchronized (mAccountList) {
    581                 for (Account account : mAccountList) {
    582                     if (!first) {
    583                         sb.append(',');
    584                     } else {
    585                         first = false;
    586                     }
    587                     sb.append(account.mId);
    588                 }
    589             }
    590             sb.append(')');
    591             mEasAccountSelector = sb.toString();
    592         }
    593         return mEasAccountSelector;
    594     }
    595 
    596     @Override
    597     public String getAccountManagerType() {
    598         return Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE;
    599     }
    600 
    601     @Override
    602     public Intent getServiceIntent() {
    603         return mIntent;
    604     }
    605 
    606     @Override
    607     public Stub getCallbackProxy() {
    608         return null;
    609     }
    610 
    611     /**
    612      * Stop any ping in progress if required
    613      *
    614      * @param mailbox whose service has started
    615      */
    616     @Override
    617     public void onStartService(Mailbox mailbox) {
    618         // If this is a ping mailbox, stop the ping
    619         if (mailbox.mSyncInterval != Mailbox.CHECK_INTERVAL_PING) return;
    620         long accountMailboxId = Mailbox.findMailboxOfType(this, mailbox.mAccountKey,
    621                 Mailbox.TYPE_EAS_ACCOUNT_MAILBOX);
    622         // If our ping is running, stop it
    623         final AbstractSyncService svc = getRunningService(accountMailboxId);
    624         if (svc != null) {
    625             log("Stopping ping due to sync of mailbox: " + mailbox.mDisplayName);
    626             // Don't block; reset might perform network activity
    627             new Thread(new Runnable() {
    628                 @Override
    629                 public void run() {
    630                     svc.reset();
    631                 }}).start();
    632         }
    633     }
    634 }
    635