Home | History | Annotate | Download | only in service
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.email.service;
     18 
     19 import android.accounts.AccountManager;
     20 import android.accounts.AccountManagerCallback;
     21 import android.accounts.AccountManagerFuture;
     22 import android.accounts.AuthenticatorException;
     23 import android.accounts.OperationCanceledException;
     24 import android.app.Service;
     25 import android.content.ContentProviderClient;
     26 import android.content.ContentResolver;
     27 import android.content.ContentUris;
     28 import android.content.ContentValues;
     29 import android.content.Context;
     30 import android.content.Intent;
     31 import android.content.pm.ActivityInfo;
     32 import android.content.res.Configuration;
     33 import android.content.res.Resources;
     34 import android.content.res.TypedArray;
     35 import android.content.res.XmlResourceParser;
     36 import android.database.Cursor;
     37 import android.net.Uri;
     38 import android.os.Bundle;
     39 import android.os.IBinder;
     40 import android.os.RemoteException;
     41 import android.provider.CalendarContract;
     42 import android.provider.CalendarContract.Calendars;
     43 import android.provider.CalendarContract.SyncState;
     44 import android.provider.ContactsContract;
     45 import android.provider.ContactsContract.RawContacts;
     46 import android.provider.SyncStateContract;
     47 
     48 import com.android.email.R;
     49 import com.android.emailcommon.Api;
     50 import com.android.emailcommon.Logging;
     51 import com.android.emailcommon.provider.Account;
     52 import com.android.emailcommon.provider.EmailContent;
     53 import com.android.emailcommon.provider.EmailContent.AccountColumns;
     54 import com.android.emailcommon.provider.HostAuth;
     55 import com.android.emailcommon.service.EmailServiceProxy;
     56 import com.android.emailcommon.service.IEmailService;
     57 import com.android.emailcommon.service.IEmailServiceCallback;
     58 import com.android.emailcommon.service.SearchParams;
     59 import com.android.emailcommon.service.ServiceProxy;
     60 import com.android.emailcommon.service.SyncWindow;
     61 import com.android.mail.utils.LogUtils;
     62 import com.google.common.collect.ImmutableMap;
     63 
     64 import org.xmlpull.v1.XmlPullParserException;
     65 
     66 import java.io.IOException;
     67 import java.util.Collection;
     68 import java.util.Map;
     69 
     70 /**
     71  * Utility functions for EmailService support.
     72  */
     73 public class EmailServiceUtils {
     74     /**
     75      * Ask a service to kill its process. This is used when an account is deleted so that
     76      * no background thread that happens to be running will continue, possibly hitting an
     77      * NPE or other error when trying to operate on an account that no longer exists.
     78      * TODO: This is kind of a hack, it's only needed because we fail so badly if an account
     79      * is deleted out from under us while a sync or other operation is in progress. It would
     80      * be a lot cleaner if our background services could handle this without crashing.
     81      */
     82     public static void killService(Context context, String protocol) {
     83         EmailServiceInfo info = getServiceInfo(context, protocol);
     84         if (info != null && info.intentAction != null) {
     85             final Intent serviceIntent = getServiceIntent(info);
     86             serviceIntent.putExtra(ServiceProxy.EXTRA_FORCE_SHUTDOWN, true);
     87             context.startService(serviceIntent);
     88         }
     89     }
     90 
     91     /**
     92      * Starts an EmailService by protocol
     93      */
     94     public static void startService(Context context, String protocol) {
     95         EmailServiceInfo info = getServiceInfo(context, protocol);
     96         if (info != null && info.intentAction != null) {
     97             final Intent serviceIntent = getServiceIntent(info);
     98             context.startService(serviceIntent);
     99         }
    100     }
    101 
    102     /**
    103      * Starts all remote services
    104      */
    105     public static void startRemoteServices(Context context) {
    106         for (EmailServiceInfo info: getServiceInfoList(context)) {
    107             if (info.intentAction != null) {
    108                 final Intent serviceIntent = getServiceIntent(info);
    109                 context.startService(serviceIntent);
    110             }
    111         }
    112     }
    113 
    114     /**
    115      * Returns whether or not remote services are present on device
    116      */
    117     public static boolean areRemoteServicesInstalled(Context context) {
    118         for (EmailServiceInfo info: getServiceInfoList(context)) {
    119             if (info.intentAction != null) {
    120                 return true;
    121             }
    122         }
    123         return false;
    124     }
    125 
    126     /**
    127      * Starts all remote services
    128      */
    129     public static void setRemoteServicesLogging(Context context, int debugBits) {
    130         for (EmailServiceInfo info: getServiceInfoList(context)) {
    131             if (info.intentAction != null) {
    132                 EmailServiceProxy service =
    133                         EmailServiceUtils.getService(context, info.protocol);
    134                 if (service != null) {
    135                     try {
    136                         service.setLogging(debugBits);
    137                     } catch (RemoteException e) {
    138                         // Move along, nothing to see
    139                     }
    140                 }
    141             }
    142         }
    143     }
    144 
    145     /**
    146      * Determine if the EmailService is available
    147      */
    148     public static boolean isServiceAvailable(Context context, String protocol) {
    149         EmailServiceInfo info = getServiceInfo(context, protocol);
    150         if (info == null) return false;
    151         if (info.klass != null) return true;
    152         final Intent serviceIntent = getServiceIntent(info);
    153         return new EmailServiceProxy(context, serviceIntent).test();
    154     }
    155 
    156     private static Intent getServiceIntent(EmailServiceInfo info) {
    157         final Intent serviceIntent = new Intent(info.intentAction);
    158         serviceIntent.setPackage(info.intentPackage);
    159         return serviceIntent;
    160     }
    161 
    162     /**
    163      * For a given account id, return a service proxy if applicable, or null.
    164      *
    165      * @param accountId the message of interest
    166      * @return service proxy, or null if n/a
    167      */
    168     public static EmailServiceProxy getServiceForAccount(Context context, long accountId) {
    169         return getService(context, Account.getProtocol(context, accountId));
    170     }
    171 
    172     /**
    173      * Holder of service information (currently just name and class/intent); if there is a class
    174      * member, this is a (local, i.e. same process) service; otherwise, this is a remote service
    175      */
    176     public static class EmailServiceInfo {
    177         public String protocol;
    178         public String name;
    179         public String accountType;
    180         Class<? extends Service> klass;
    181         String intentAction;
    182         String intentPackage;
    183         public int port;
    184         public int portSsl;
    185         public boolean defaultSsl;
    186         public boolean offerTls;
    187         public boolean offerCerts;
    188         public boolean usesSmtp;
    189         public boolean offerLocalDeletes;
    190         public int defaultLocalDeletes;
    191         public boolean offerPrefix;
    192         public boolean usesAutodiscover;
    193         public boolean offerLookback;
    194         public int defaultLookback;
    195         public boolean syncChanges;
    196         public boolean syncContacts;
    197         public boolean syncCalendar;
    198         public boolean offerAttachmentPreload;
    199         public CharSequence[] syncIntervalStrings;
    200         public CharSequence[] syncIntervals;
    201         public int defaultSyncInterval;
    202         public String inferPrefix;
    203         public boolean offerLoadMore;
    204         public boolean offerMoveTo;
    205         public boolean requiresSetup;
    206         public boolean hide;
    207 
    208         @Override
    209         public String toString() {
    210             StringBuilder sb = new StringBuilder("Protocol: ");
    211             sb.append(protocol);
    212             sb.append(", ");
    213             sb.append(klass != null ? "Local" : "Remote");
    214             sb.append(" , Account Type: ");
    215             sb.append(accountType);
    216             return sb.toString();
    217         }
    218     }
    219 
    220     public static EmailServiceProxy getService(Context context, String protocol) {
    221         EmailServiceInfo info = null;
    222         // Handle the degenerate case here (account might have been deleted)
    223         if (protocol != null) {
    224             info = getServiceInfo(context, protocol);
    225         }
    226         if (info == null) {
    227             LogUtils.w(Logging.LOG_TAG, "Returning NullService for " + protocol);
    228             return new EmailServiceProxy(context, NullService.class);
    229         } else  {
    230             return getServiceFromInfo(context, info);
    231         }
    232     }
    233 
    234     public static EmailServiceProxy getServiceFromInfo(Context context, EmailServiceInfo info) {
    235         if (info.klass != null) {
    236             return new EmailServiceProxy(context, info.klass);
    237         } else {
    238             final Intent serviceIntent = getServiceIntent(info);
    239             return new EmailServiceProxy(context, serviceIntent);
    240         }
    241     }
    242 
    243     public static EmailServiceInfo getServiceInfoForAccount(Context context, long accountId) {
    244         String protocol = Account.getProtocol(context, accountId);
    245         return getServiceInfo(context, protocol);
    246     }
    247 
    248     public static EmailServiceInfo getServiceInfo(Context context, String protocol) {
    249         return getServiceMap(context).get(protocol);
    250     }
    251 
    252     public static Collection<EmailServiceInfo> getServiceInfoList(Context context) {
    253         return getServiceMap(context).values();
    254     }
    255 
    256     private static void finishAccountManagerBlocker(AccountManagerFuture<?> future) {
    257         try {
    258             // Note: All of the potential errors are simply logged
    259             // here, as there is nothing to actually do about them.
    260             future.getResult();
    261         } catch (OperationCanceledException e) {
    262             LogUtils.w(Logging.LOG_TAG, e.toString());
    263         } catch (AuthenticatorException e) {
    264             LogUtils.w(Logging.LOG_TAG, e.toString());
    265         } catch (IOException e) {
    266             LogUtils.w(Logging.LOG_TAG, e.toString());
    267         }
    268     }
    269 
    270     /**
    271      * Add an account to the AccountManager.
    272      * @param context Our {@link Context}.
    273      * @param account The {@link Account} we're adding.
    274      * @param email Whether the user wants to sync email on this account.
    275      * @param calendar Whether the user wants to sync calendar on this account.
    276      * @param contacts Whether the user wants to sync contacts on this account.
    277      * @param callback A callback for when the AccountManager is done.
    278      * @return The result of {@link AccountManager#addAccount}.
    279      */
    280     public static AccountManagerFuture<Bundle> setupAccountManagerAccount(final Context context,
    281             final Account account, final boolean email, final boolean calendar,
    282             final boolean contacts, final AccountManagerCallback<Bundle> callback) {
    283         final Bundle options = new Bundle(5);
    284         final HostAuth hostAuthRecv =
    285                 HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
    286         if (hostAuthRecv == null) {
    287             return null;
    288         }
    289         // Set up username/password
    290         options.putString(EasAuthenticatorService.OPTIONS_USERNAME, account.mEmailAddress);
    291         options.putString(EasAuthenticatorService.OPTIONS_PASSWORD, hostAuthRecv.mPassword);
    292         options.putBoolean(EasAuthenticatorService.OPTIONS_CONTACTS_SYNC_ENABLED, contacts);
    293         options.putBoolean(EasAuthenticatorService.OPTIONS_CALENDAR_SYNC_ENABLED, calendar);
    294         options.putBoolean(EasAuthenticatorService.OPTIONS_EMAIL_SYNC_ENABLED, email);
    295         final EmailServiceInfo info = getServiceInfo(context, hostAuthRecv.mProtocol);
    296         return AccountManager.get(context).addAccount(info.accountType, null, null, options, null,
    297                 callback, null);
    298     }
    299 
    300     public static void updateAccountManagerType(Context context,
    301             android.accounts.Account amAccount, final Map<String, String> protocolMap) {
    302         final ContentResolver resolver = context.getContentResolver();
    303         final Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION,
    304                 AccountColumns.EMAIL_ADDRESS + "=?", new String[] { amAccount.name }, null);
    305         // That's odd, isn't it?
    306         if (c == null) return;
    307         try {
    308             if (c.moveToNext()) {
    309                 // Get the EmailProvider Account/HostAuth
    310                 final Account account = new Account();
    311                 account.restore(c);
    312                 final HostAuth hostAuth =
    313                         HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
    314                 if (hostAuth == null) {
    315                     return;
    316                 }
    317 
    318                 final String newProtocol = protocolMap.get(hostAuth.mProtocol);
    319                 if (newProtocol == null) {
    320                     // This account doesn't need updating.
    321                     return;
    322                 }
    323 
    324                 LogUtils.w(Logging.LOG_TAG, "Converting " + amAccount.name + " to "
    325                         + newProtocol);
    326 
    327                 final ContentValues accountValues = new ContentValues();
    328                 int oldFlags = account.mFlags;
    329 
    330                 // Mark the provider account incomplete so it can't get reconciled away
    331                 account.mFlags |= Account.FLAGS_INCOMPLETE;
    332                 accountValues.put(AccountColumns.FLAGS, account.mFlags);
    333                 final Uri accountUri = ContentUris.withAppendedId(Account.CONTENT_URI, account.mId);
    334                 resolver.update(accountUri, accountValues, null, null);
    335 
    336                 // Change the HostAuth to reference the new protocol; this has to be done before
    337                 // trying to create the AccountManager account (below)
    338                 final ContentValues hostValues = new ContentValues();
    339                 hostValues.put(HostAuth.PROTOCOL, newProtocol);
    340                 resolver.update(ContentUris.withAppendedId(HostAuth.CONTENT_URI, hostAuth.mId),
    341                         hostValues, null, null);
    342                 LogUtils.w(Logging.LOG_TAG, "Updated HostAuths");
    343 
    344                 try {
    345                     // Get current settings for the existing AccountManager account
    346                     boolean email = ContentResolver.getSyncAutomatically(amAccount,
    347                             EmailContent.AUTHORITY);
    348                     if (!email) {
    349                         // Try our old provider name
    350                         email = ContentResolver.getSyncAutomatically(amAccount,
    351                                 "com.android.email.provider");
    352                     }
    353                     final boolean contacts = ContentResolver.getSyncAutomatically(amAccount,
    354                             ContactsContract.AUTHORITY);
    355                     final boolean calendar = ContentResolver.getSyncAutomatically(amAccount,
    356                             CalendarContract.AUTHORITY);
    357                     LogUtils.w(Logging.LOG_TAG, "Email: " + email + ", Contacts: " + contacts + ","
    358                             + " Calendar: " + calendar);
    359 
    360                     // Get sync keys for calendar/contacts
    361                     final String amName = amAccount.name;
    362                     final String oldType = amAccount.type;
    363                     ContentProviderClient client = context.getContentResolver()
    364                             .acquireContentProviderClient(CalendarContract.CONTENT_URI);
    365                     byte[] calendarSyncKey = null;
    366                     try {
    367                         calendarSyncKey = SyncStateContract.Helpers.get(client,
    368                                 asCalendarSyncAdapter(SyncState.CONTENT_URI, amName, oldType),
    369                                 new android.accounts.Account(amName, oldType));
    370                     } catch (RemoteException e) {
    371                         LogUtils.w(Logging.LOG_TAG, "Get calendar key FAILED");
    372                     } finally {
    373                         client.release();
    374                     }
    375                     client = context.getContentResolver()
    376                             .acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
    377                     byte[] contactsSyncKey = null;
    378                     try {
    379                         contactsSyncKey = SyncStateContract.Helpers.get(client,
    380                                 ContactsContract.SyncState.CONTENT_URI,
    381                                 new android.accounts.Account(amName, oldType));
    382                     } catch (RemoteException e) {
    383                         LogUtils.w(Logging.LOG_TAG, "Get contacts key FAILED");
    384                     } finally {
    385                         client.release();
    386                     }
    387                     if (calendarSyncKey != null) {
    388                         LogUtils.w(Logging.LOG_TAG, "Got calendar key: "
    389                                 + new String(calendarSyncKey));
    390                     }
    391                     if (contactsSyncKey != null) {
    392                         LogUtils.w(Logging.LOG_TAG, "Got contacts key: "
    393                                 + new String(contactsSyncKey));
    394                     }
    395 
    396                     // Set up a new AccountManager account with new type and old settings
    397                     AccountManagerFuture<?> amFuture = setupAccountManagerAccount(context, account,
    398                             email, calendar, contacts, null);
    399                     finishAccountManagerBlocker(amFuture);
    400                     LogUtils.w(Logging.LOG_TAG, "Created new AccountManager account");
    401 
    402                     // TODO: Clean up how we determine the type.
    403                     final String accountType = protocolMap.get(hostAuth.mProtocol + "_type");
    404                     // Move calendar and contacts data from the old account to the new one.
    405                     // We must do this before deleting the old account or the data is lost.
    406                     moveCalendarData(context.getContentResolver(), amName, oldType, accountType);
    407                     moveContactsData(context.getContentResolver(), amName, oldType, accountType);
    408 
    409                     // Delete the AccountManager account
    410                     amFuture = AccountManager.get(context)
    411                             .removeAccount(amAccount, null, null);
    412                     finishAccountManagerBlocker(amFuture);
    413                     LogUtils.w(Logging.LOG_TAG, "Deleted old AccountManager account");
    414 
    415                     // Restore sync keys for contacts/calendar
    416 
    417                     if (accountType != null &&
    418                             calendarSyncKey != null && calendarSyncKey.length != 0) {
    419                         client = context.getContentResolver()
    420                                 .acquireContentProviderClient(CalendarContract.CONTENT_URI);
    421                         try {
    422                             SyncStateContract.Helpers.set(client,
    423                                     asCalendarSyncAdapter(SyncState.CONTENT_URI, amName,
    424                                             accountType),
    425                                     new android.accounts.Account(amName, accountType),
    426                                     calendarSyncKey);
    427                             LogUtils.w(Logging.LOG_TAG, "Set calendar key...");
    428                         } catch (RemoteException e) {
    429                             LogUtils.w(Logging.LOG_TAG, "Set calendar key FAILED");
    430                         } finally {
    431                             client.release();
    432                         }
    433                     }
    434                     if (accountType != null &&
    435                             contactsSyncKey != null && contactsSyncKey.length != 0) {
    436                         client = context.getContentResolver()
    437                                 .acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
    438                         try {
    439                             SyncStateContract.Helpers.set(client,
    440                                     ContactsContract.SyncState.CONTENT_URI,
    441                                     new android.accounts.Account(amName, accountType),
    442                                     contactsSyncKey);
    443                             LogUtils.w(Logging.LOG_TAG, "Set contacts key...");
    444                         } catch (RemoteException e) {
    445                             LogUtils.w(Logging.LOG_TAG, "Set contacts key FAILED");
    446                         }
    447                     }
    448 
    449                     // That's all folks!
    450                     LogUtils.w(Logging.LOG_TAG, "Account update completed.");
    451                 } finally {
    452                     // Clear the incomplete flag on the provider account
    453                     accountValues.put(AccountColumns.FLAGS, oldFlags);
    454                     resolver.update(accountUri, accountValues, null, null);
    455                     LogUtils.w(Logging.LOG_TAG, "[Incomplete flag cleared]");
    456                 }
    457             }
    458         } finally {
    459             c.close();
    460         }
    461     }
    462 
    463     private static void moveCalendarData(final ContentResolver resolver, final String name,
    464             final String oldType, final String newType) {
    465         final Uri oldCalendars = Calendars.CONTENT_URI.buildUpon()
    466                 .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
    467                 .appendQueryParameter(Calendars.ACCOUNT_NAME, name)
    468                 .appendQueryParameter(Calendars.ACCOUNT_TYPE, oldType)
    469                 .build();
    470 
    471         // Update this calendar to have the new account type.
    472         final ContentValues values = new ContentValues();
    473         values.put(CalendarContract.Calendars.ACCOUNT_TYPE, newType);
    474         resolver.update(oldCalendars, values,
    475                 Calendars.ACCOUNT_NAME + "=? AND " + Calendars.ACCOUNT_TYPE + "=?",
    476                 new String[] {name, oldType});
    477     }
    478 
    479     private static void moveContactsData(final ContentResolver resolver, final String name,
    480             final String oldType, final String newType) {
    481         final Uri oldContacts = RawContacts.CONTENT_URI.buildUpon()
    482                 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
    483                 .appendQueryParameter(RawContacts.ACCOUNT_NAME, name)
    484                 .appendQueryParameter(RawContacts.ACCOUNT_TYPE, oldType)
    485                 .build();
    486 
    487         // Update this calendar to have the new account type.
    488         final ContentValues values = new ContentValues();
    489         values.put(CalendarContract.Calendars.ACCOUNT_TYPE, newType);
    490         resolver.update(oldContacts, values, null, null);
    491     }
    492 
    493     private static final Configuration sOldConfiguration = new Configuration();
    494     private static Map<String, EmailServiceInfo> sServiceMap = null;
    495     private static final Object sServiceMapLock = new Object();
    496 
    497     /**
    498      * Parse services.xml file to find our available email services
    499      */
    500     private static Map<String, EmailServiceInfo> getServiceMap(final Context context) {
    501         synchronized (sServiceMapLock) {
    502             /**
    503              * We cache localized strings here, so make sure to regenerate the service map if
    504              * the locale changes
    505              */
    506             if (sServiceMap == null) {
    507                 sOldConfiguration.setTo(context.getResources().getConfiguration());
    508             }
    509 
    510             final int delta =
    511                     sOldConfiguration.updateFrom(context.getResources().getConfiguration());
    512 
    513             if (sServiceMap != null
    514                     && !Configuration.needNewResources(delta, ActivityInfo.CONFIG_LOCALE)) {
    515                 return sServiceMap;
    516             }
    517 
    518             final ImmutableMap.Builder<String, EmailServiceInfo> builder = ImmutableMap.builder();
    519 
    520             try {
    521                 final Resources res = context.getResources();
    522                 final XmlResourceParser xml = res.getXml(R.xml.services);
    523                 int xmlEventType;
    524                 // walk through senders.xml file.
    525                 while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) {
    526                     if (xmlEventType == XmlResourceParser.START_TAG &&
    527                             "emailservice".equals(xml.getName())) {
    528                         final EmailServiceInfo info = new EmailServiceInfo();
    529                         final TypedArray ta =
    530                                 res.obtainAttributes(xml, R.styleable.EmailServiceInfo);
    531                         info.protocol = ta.getString(R.styleable.EmailServiceInfo_protocol);
    532                         info.accountType = ta.getString(R.styleable.EmailServiceInfo_accountType);
    533                         info.name = ta.getString(R.styleable.EmailServiceInfo_name);
    534                         info.hide = ta.getBoolean(R.styleable.EmailServiceInfo_hide, false);
    535                         final String klass =
    536                                 ta.getString(R.styleable.EmailServiceInfo_serviceClass);
    537                         info.intentAction = ta.getString(R.styleable.EmailServiceInfo_intent);
    538                         info.intentPackage =
    539                                 ta.getString(R.styleable.EmailServiceInfo_intentPackage);
    540                         info.defaultSsl =
    541                                 ta.getBoolean(R.styleable.EmailServiceInfo_defaultSsl, false);
    542                         info.port = ta.getInteger(R.styleable.EmailServiceInfo_port, 0);
    543                         info.portSsl = ta.getInteger(R.styleable.EmailServiceInfo_portSsl, 0);
    544                         info.offerTls = ta.getBoolean(R.styleable.EmailServiceInfo_offerTls, false);
    545                         info.offerCerts =
    546                                 ta.getBoolean(R.styleable.EmailServiceInfo_offerCerts, false);
    547                         info.offerLocalDeletes =
    548                             ta.getBoolean(R.styleable.EmailServiceInfo_offerLocalDeletes, false);
    549                         info.defaultLocalDeletes =
    550                             ta.getInteger(R.styleable.EmailServiceInfo_defaultLocalDeletes,
    551                                     Account.DELETE_POLICY_ON_DELETE);
    552                         info.offerPrefix =
    553                             ta.getBoolean(R.styleable.EmailServiceInfo_offerPrefix, false);
    554                         info.usesSmtp = ta.getBoolean(R.styleable.EmailServiceInfo_usesSmtp, false);
    555                         info.usesAutodiscover =
    556                             ta.getBoolean(R.styleable.EmailServiceInfo_usesAutodiscover, false);
    557                         info.offerLookback =
    558                             ta.getBoolean(R.styleable.EmailServiceInfo_offerLookback, false);
    559                         info.defaultLookback =
    560                             ta.getInteger(R.styleable.EmailServiceInfo_defaultLookback,
    561                                     SyncWindow.SYNC_WINDOW_3_DAYS);
    562                         info.syncChanges =
    563                             ta.getBoolean(R.styleable.EmailServiceInfo_syncChanges, false);
    564                         info.syncContacts =
    565                             ta.getBoolean(R.styleable.EmailServiceInfo_syncContacts, false);
    566                         info.syncCalendar =
    567                             ta.getBoolean(R.styleable.EmailServiceInfo_syncCalendar, false);
    568                         info.offerAttachmentPreload =
    569                             ta.getBoolean(R.styleable.EmailServiceInfo_offerAttachmentPreload,
    570                                     false);
    571                         info.syncIntervalStrings =
    572                             ta.getTextArray(R.styleable.EmailServiceInfo_syncIntervalStrings);
    573                         info.syncIntervals =
    574                             ta.getTextArray(R.styleable.EmailServiceInfo_syncIntervals);
    575                         info.defaultSyncInterval =
    576                             ta.getInteger(R.styleable.EmailServiceInfo_defaultSyncInterval, 15);
    577                         info.inferPrefix = ta.getString(R.styleable.EmailServiceInfo_inferPrefix);
    578                         info.offerLoadMore =
    579                                 ta.getBoolean(R.styleable.EmailServiceInfo_offerLoadMore, false);
    580                         info.offerMoveTo =
    581                                 ta.getBoolean(R.styleable.EmailServiceInfo_offerMoveTo, false);
    582                         info.requiresSetup =
    583                                 ta.getBoolean(R.styleable.EmailServiceInfo_requiresSetup, false);
    584 
    585                         // Must have either "class" (local) or "intent" (remote)
    586                         if (klass != null) {
    587                             try {
    588                                 // noinspection unchecked
    589                                 info.klass = (Class<? extends Service>) Class.forName(klass);
    590                             } catch (ClassNotFoundException e) {
    591                                 throw new IllegalStateException(
    592                                         "Class not found in service descriptor: " + klass);
    593                             }
    594                         }
    595                         if (info.klass == null && info.intentAction == null) {
    596                             throw new IllegalStateException(
    597                                     "No class or intent action specified in service descriptor");
    598                         }
    599                         if (info.klass != null && info.intentAction != null) {
    600                             throw new IllegalStateException(
    601                                     "Both class and intent action specified in service descriptor");
    602                         }
    603                         builder.put(info.protocol, info);
    604                     }
    605                 }
    606             } catch (XmlPullParserException e) {
    607                 // ignore
    608             } catch (IOException e) {
    609                 // ignore
    610             }
    611             sServiceMap = builder.build();
    612             return sServiceMap;
    613         }
    614     }
    615 
    616     private static Uri asCalendarSyncAdapter(Uri uri, String account, String accountType) {
    617         return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
    618                 .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
    619                 .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
    620     }
    621 
    622     /**
    623      * A no-op service that can be returned for non-existent/null protocols
    624      */
    625     class NullService implements IEmailService {
    626         @Override
    627         public IBinder asBinder() {
    628             return null;
    629         }
    630 
    631         @Override
    632         public Bundle validate(HostAuth hostauth) throws RemoteException {
    633             return null;
    634         }
    635 
    636         @Override
    637         public void startSync(long mailboxId, boolean userRequest, int deltaMessageCount)
    638                 throws RemoteException {
    639         }
    640 
    641         @Override
    642         public void stopSync(long mailboxId) throws RemoteException {
    643         }
    644 
    645         @Override
    646         public void loadMore(long messageId) throws RemoteException {
    647         }
    648 
    649         @Override
    650         public void loadAttachment(final IEmailServiceCallback cb, final long attachmentId,
    651                 final boolean background) throws RemoteException {
    652         }
    653 
    654         @Override
    655         public void updateFolderList(long accountId) throws RemoteException {
    656         }
    657 
    658         @Override
    659         public boolean createFolder(long accountId, String name) throws RemoteException {
    660             return false;
    661         }
    662 
    663         @Override
    664         public boolean deleteFolder(long accountId, String name) throws RemoteException {
    665             return false;
    666         }
    667 
    668         @Override
    669         public boolean renameFolder(long accountId, String oldName, String newName)
    670                 throws RemoteException {
    671             return false;
    672         }
    673 
    674         @Override
    675         public void setLogging(int on) throws RemoteException {
    676         }
    677 
    678         @Override
    679         public void hostChanged(long accountId) throws RemoteException {
    680         }
    681 
    682         @Override
    683         public Bundle autoDiscover(String userName, String password) throws RemoteException {
    684             return null;
    685         }
    686 
    687         @Override
    688         public void sendMeetingResponse(long messageId, int response) throws RemoteException {
    689         }
    690 
    691         @Override
    692         public void deleteAccountPIMData(final String emailAddress) throws RemoteException {
    693         }
    694 
    695         @Override
    696         public int getApiLevel() throws RemoteException {
    697             return Api.LEVEL;
    698         }
    699 
    700         @Override
    701         public int searchMessages(long accountId, SearchParams params, long destMailboxId)
    702                 throws RemoteException {
    703             return 0;
    704         }
    705 
    706         @Override
    707         public void sendMail(long accountId) throws RemoteException {
    708         }
    709 
    710         @Override
    711         public void serviceUpdated(String emailAddress) throws RemoteException {
    712         }
    713 
    714         @Override
    715         public int getCapabilities(Account acct) throws RemoteException {
    716             return 0;
    717         }
    718     }
    719 }
    720