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