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