summaryrefslogtreecommitdiffstats
path: root/src/com/android/email/service/EmailServiceUtils.java
blob: a2cda8d50eb5c1e86f118c00c60f10585aa020fe (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.email.service;

import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.Service;
import android.content.ComponentName;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Calendars;
import android.provider.CalendarContract.SyncState;
import android.provider.ContactsContract;
import android.provider.ContactsContract.RawContacts;
import android.provider.SyncStateContract;
import android.support.annotation.Nullable;
import android.text.TextUtils;

import com.android.email.R;
import com.android.emailcommon.VendorPolicyLoader;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.AccountColumns;
import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.service.EmailServiceStatus;
import com.android.emailcommon.service.EmailServiceVersion;
import com.android.emailcommon.service.HostAuthCompat;
import com.android.emailcommon.service.IEmailService;
import com.android.emailcommon.service.IEmailServiceCallback;
import com.android.emailcommon.service.SearchParams;
import com.android.emailcommon.service.ServiceProxy;
import com.android.emailcommon.service.SyncWindow;
import com.android.mail.utils.LogUtils;
import com.google.common.collect.ImmutableMap;

import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.util.Collection;
import java.util.Map;

/**
 * Utility functions for EmailService support.
 */
public class EmailServiceUtils {
    /**
     * Ask a service to kill its process. This is used when an account is deleted so that
     * no background thread that happens to be running will continue, possibly hitting an
     * NPE or other error when trying to operate on an account that no longer exists.
     * TODO: This is kind of a hack, it's only needed because we fail so badly if an account
     * is deleted out from under us while a sync or other operation is in progress. It would
     * be a lot cleaner if our background services could handle this without crashing.
     */
    public static void killService(Context context, String protocol) {
        EmailServiceInfo info = getServiceInfo(context, protocol);
        if (info != null && info.intentAction != null) {
            final Intent serviceIntent = getServiceIntent(info);
            serviceIntent.putExtra(ServiceProxy.EXTRA_FORCE_SHUTDOWN, true);
            context.startService(serviceIntent);
        }
    }

    /**
     * Starts an EmailService by protocol
     */
    public static void startService(Context context, String protocol) {
        EmailServiceInfo info = getServiceInfo(context, protocol);
        if (info != null && info.intentAction != null) {
            final Intent serviceIntent = getServiceIntent(info);
            context.startService(serviceIntent);
        }
    }

    /**
     * Starts all remote services
     */
    public static void startRemoteServices(Context context) {
        for (EmailServiceInfo info: getServiceInfoList(context)) {
            if (info.intentAction != null) {
                final Intent serviceIntent = getServiceIntent(info);
                context.startService(serviceIntent);
            }
        }
    }

    /**
     * Returns whether or not remote services are present on device
     */
    public static boolean areRemoteServicesInstalled(Context context) {
        for (EmailServiceInfo info: getServiceInfoList(context)) {
            if (info.intentAction != null) {
                return true;
            }
        }
        return false;
    }

    /**
     * Starts all remote services
     */
    public static void setRemoteServicesLogging(Context context, int debugBits) {
        for (EmailServiceInfo info: getServiceInfoList(context)) {
            if (info.intentAction != null) {
                EmailServiceProxy service =
                        EmailServiceUtils.getService(context, info.protocol);
                if (service != null) {
                    try {
                        service.setLogging(debugBits);
                    } catch (RemoteException e) {
                        // Move along, nothing to see
                    }
                }
            }
        }
    }

    /**
     * Determine if the EmailService is available
     */
    public static boolean isServiceAvailable(Context context, String protocol) {
        EmailServiceInfo info = getServiceInfo(context, protocol);
        if (info == null) return false;
        if (info.klass != null) return true;
        final Intent serviceIntent = getServiceIntent(info);
        return new EmailServiceProxy(context, serviceIntent).test();
    }

    private static Intent getServiceIntent(EmailServiceInfo info) {
        final Intent serviceIntent = new Intent(info.intentAction);
        serviceIntent.setPackage(info.intentPackage);
        return serviceIntent;
    }

    /**
     * For a given account id, return a service proxy if applicable, or null.
     *
     * @param accountId the message of interest
     * @return service proxy, or null if n/a
     */
    public static EmailServiceProxy getServiceForAccount(Context context, long accountId) {
        return getService(context, Account.getProtocol(context, accountId));
    }

    /**
     * Holder of service information (currently just name and class/intent); if there is a class
     * member, this is a (local, i.e. same process) service; otherwise, this is a remote service
     */
    public static class EmailServiceInfo {
        public String protocol;
        public String name;
        public String accountType;
        Class<? extends Service> klass;
        String intentAction;
        String intentPackage;
        public int port;
        public int portSsl;
        public boolean defaultSsl;
        public boolean offerTls;
        public boolean offerCerts;
        public boolean offerOAuth;
        public boolean usesSmtp;
        public boolean offerLocalDeletes;
        public int defaultLocalDeletes;
        public boolean offerPrefix;
        public boolean usesAutodiscover;
        public boolean offerLookback;
        public int defaultLookback;
        public boolean syncChanges;
        public boolean syncContacts;
        public boolean syncCalendar;
        public boolean offerAttachmentPreload;
        public CharSequence[] syncIntervalStrings;
        public CharSequence[] syncIntervals;
        public int defaultSyncInterval;
        public String inferPrefix;
        public boolean offerLoadMore;
        public boolean offerMoveTo;
        public boolean requiresSetup;
        public boolean hide;
        public boolean isGmailStub;

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder("Protocol: ");
            sb.append(protocol);
            sb.append(", ");
            sb.append(klass != null ? "Local" : "Remote");
            sb.append(" , Account Type: ");
            sb.append(accountType);
            return sb.toString();
        }
    }

    public static EmailServiceProxy getService(Context context, String protocol) {
        EmailServiceInfo info = null;
        // Handle the degenerate case here (account might have been deleted)
        if (protocol != null) {
            info = getServiceInfo(context, protocol);
        }
        if (info == null) {
            LogUtils.w(LogUtils.TAG, "Returning NullService for %s", protocol);
            return new EmailServiceProxy(context, NullService.class);
        } else  {
            return getServiceFromInfo(context, info);
        }
    }

    public static EmailServiceProxy getServiceFromInfo(Context context, EmailServiceInfo info) {
        if (info.klass != null) {
            return new EmailServiceProxy(context, info.klass);
        } else {
            final Intent serviceIntent = getServiceIntent(info);
            return new EmailServiceProxy(context, serviceIntent);
        }
    }

    public static EmailServiceInfo getServiceInfoForAccount(Context context, long accountId) {
        String protocol = Account.getProtocol(context, accountId);
        return getServiceInfo(context, protocol);
    }

    public static EmailServiceInfo getServiceInfo(Context context, String protocol) {
        return getServiceMap(context).get(protocol);
    }

    public static Collection<EmailServiceInfo> getServiceInfoList(Context context) {
        return getServiceMap(context).values();
    }

    private static void finishAccountManagerBlocker(AccountManagerFuture<?> future) {
        try {
            // Note: All of the potential errors are simply logged
            // here, as there is nothing to actually do about them.
            future.getResult();
        } catch (OperationCanceledException e) {
            LogUtils.w(LogUtils.TAG, e, "finishAccountManagerBlocker");
        } catch (AuthenticatorException e) {
            LogUtils.w(LogUtils.TAG, e, "finishAccountManagerBlocker");
        } catch (IOException e) {
            LogUtils.w(LogUtils.TAG, e, "finishAccountManagerBlocker");
        }
    }

    /**
     * Add an account to the AccountManager.
     * @param context Our {@link Context}.
     * @param account The {@link Account} we're adding.
     * @param email Whether the user wants to sync email on this account.
     * @param calendar Whether the user wants to sync calendar on this account.
     * @param contacts Whether the user wants to sync contacts on this account.
     * @param callback A callback for when the AccountManager is done.
     * @return The result of {@link AccountManager#addAccount}.
     */
    public static AccountManagerFuture<Bundle> setupAccountManagerAccount(final Context context,
            final Account account, final boolean email, final boolean calendar,
            final boolean contacts, final AccountManagerCallback<Bundle> callback) {
        final HostAuth hostAuthRecv =
                HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
        return setupAccountManagerAccount(context, account, email, calendar, contacts,
                hostAuthRecv, callback);
    }

    /**
     * Add an account to the AccountManager.
     * @param context Our {@link Context}.
     * @param account The {@link Account} we're adding.
     * @param email Whether the user wants to sync email on this account.
     * @param calendar Whether the user wants to sync calendar on this account.
     * @param contacts Whether the user wants to sync contacts on this account.
     * @param hostAuth HostAuth that identifies the protocol and password for this account.
     * @param callback A callback for when the AccountManager is done.
     * @return The result of {@link AccountManager#addAccount}.
     */
    public static AccountManagerFuture<Bundle> setupAccountManagerAccount(final Context context,
            final Account account, final boolean email, final boolean calendar,
            final boolean contacts, final HostAuth hostAuth,
            final AccountManagerCallback<Bundle> callback) {
        if (hostAuth == null) {
            return null;
        }
        // Set up username/password
        final Bundle options = new Bundle(5);
        options.putString(EasAuthenticatorService.OPTIONS_USERNAME, account.mEmailAddress);
        options.putString(EasAuthenticatorService.OPTIONS_PASSWORD, hostAuth.mPassword);
        options.putBoolean(EasAuthenticatorService.OPTIONS_CONTACTS_SYNC_ENABLED, contacts);
        options.putBoolean(EasAuthenticatorService.OPTIONS_CALENDAR_SYNC_ENABLED, calendar);
        options.putBoolean(EasAuthenticatorService.OPTIONS_EMAIL_SYNC_ENABLED, email);
        final EmailServiceInfo info = getServiceInfo(context, hostAuth.mProtocol);
        return AccountManager.get(context).addAccount(info.accountType, null, null, options, null,
                callback, null);
    }

    public static void updateAccountManagerType(Context context,
            android.accounts.Account amAccount, final Map<String, String> protocolMap) {
        final ContentResolver resolver = context.getContentResolver();
        final Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION,
                AccountColumns.EMAIL_ADDRESS + "=?", new String[] { amAccount.name }, null);
        // That's odd, isn't it?
        if (c == null) return;
        try {
            if (c.moveToNext()) {
                // Get the EmailProvider Account/HostAuth
                final Account account = new Account();
                account.restore(c);
                final HostAuth hostAuth =
                        HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
                if (hostAuth == null) {
                    return;
                }

                final String newProtocol = protocolMap.get(hostAuth.mProtocol);
                if (newProtocol == null) {
                    // This account doesn't need updating.
                    return;
                }

                LogUtils.w(LogUtils.TAG, "Converting %s to %s", amAccount.name, newProtocol);

                final ContentValues accountValues = new ContentValues();
                int oldFlags = account.mFlags;

                // Mark the provider account incomplete so it can't get reconciled away
                account.mFlags |= Account.FLAGS_INCOMPLETE;
                accountValues.put(AccountColumns.FLAGS, account.mFlags);
                final Uri accountUri = ContentUris.withAppendedId(Account.CONTENT_URI, account.mId);
                resolver.update(accountUri, accountValues, null, null);

                // Change the HostAuth to reference the new protocol; this has to be done before
                // trying to create the AccountManager account (below)
                final ContentValues hostValues = new ContentValues();
                hostValues.put(HostAuthColumns.PROTOCOL, newProtocol);
                resolver.update(ContentUris.withAppendedId(HostAuth.CONTENT_URI, hostAuth.mId),
                        hostValues, null, null);
                LogUtils.w(LogUtils.TAG, "Updated HostAuths");

                try {
                    // Get current settings for the existing AccountManager account
                    boolean email = ContentResolver.getSyncAutomatically(amAccount,
                            EmailContent.AUTHORITY);
                    if (!email) {
                        // Try our old provider name
                        email = ContentResolver.getSyncAutomatically(amAccount,
                                "com.android.email.provider");
                    }
                    final boolean contacts = ContentResolver.getSyncAutomatically(amAccount,
                            ContactsContract.AUTHORITY);
                    final boolean calendar = ContentResolver.getSyncAutomatically(amAccount,
                            CalendarContract.AUTHORITY);
                    LogUtils.w(LogUtils.TAG, "Email: %s, Contacts: %s Calendar: %s",
                            email, contacts, calendar);

                    // Get sync keys for calendar/contacts
                    final String amName = amAccount.name;
                    final String oldType = amAccount.type;
                    ContentProviderClient client = context.getContentResolver()
                            .acquireContentProviderClient(CalendarContract.CONTENT_URI);
                    byte[] calendarSyncKey = null;
                    try {
                        calendarSyncKey = SyncStateContract.Helpers.get(client,
                                asCalendarSyncAdapter(SyncState.CONTENT_URI, amName, oldType),
                                new android.accounts.Account(amName, oldType));
                    } catch (RemoteException e) {
                        LogUtils.w(LogUtils.TAG, "Get calendar key FAILED");
                    } finally {
                        client.release();
                    }
                    client = context.getContentResolver()
                            .acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
                    byte[] contactsSyncKey = null;
                    try {
                        contactsSyncKey = SyncStateContract.Helpers.get(client,
                                ContactsContract.SyncState.CONTENT_URI,
                                new android.accounts.Account(amName, oldType));
                    } catch (RemoteException e) {
                        LogUtils.w(LogUtils.TAG, "Get contacts key FAILED");
                    } finally {
                        client.release();
                    }
                    if (calendarSyncKey != null) {
                        LogUtils.w(LogUtils.TAG, "Got calendar key: %s",
                                new String(calendarSyncKey));
                    }
                    if (contactsSyncKey != null) {
                        LogUtils.w(LogUtils.TAG, "Got contacts key: %s",
                                new String(contactsSyncKey));
                    }

                    // Set up a new AccountManager account with new type and old settings
                    AccountManagerFuture<?> amFuture = setupAccountManagerAccount(context, account,
                            email, calendar, contacts, null);
                    finishAccountManagerBlocker(amFuture);
                    LogUtils.w(LogUtils.TAG, "Created new AccountManager account");

                    // TODO: Clean up how we determine the type.
                    final String accountType = protocolMap.get(hostAuth.mProtocol + "_type");
                    // Move calendar and contacts data from the old account to the new one.
                    // We must do this before deleting the old account or the data is lost.
                    moveCalendarData(context.getContentResolver(), amName, oldType, accountType);
                    moveContactsData(context.getContentResolver(), amName, oldType, accountType);

                    // Delete the AccountManager account
                    amFuture = AccountManager.get(context)
                            .removeAccount(amAccount, null, null);
                    finishAccountManagerBlocker(amFuture);
                    LogUtils.w(LogUtils.TAG, "Deleted old AccountManager account");

                    // Restore sync keys for contacts/calendar

                    if (accountType != null &&
                            calendarSyncKey != null && calendarSyncKey.length != 0) {
                        client = context.getContentResolver()
                                .acquireContentProviderClient(CalendarContract.CONTENT_URI);
                        try {
                            SyncStateContract.Helpers.set(client,
                                    asCalendarSyncAdapter(SyncState.CONTENT_URI, amName,
                                            accountType),
                                    new android.accounts.Account(amName, accountType),
                                    calendarSyncKey);
                            LogUtils.w(LogUtils.TAG, "Set calendar key...");
                        } catch (RemoteException e) {
                            LogUtils.w(LogUtils.TAG, "Set calendar key FAILED");
                        } finally {
                            client.release();
                        }
                    }
                    if (accountType != null &&
                            contactsSyncKey != null && contactsSyncKey.length != 0) {
                        client = context.getContentResolver()
                                .acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
                        try {
                            SyncStateContract.Helpers.set(client,
                                    ContactsContract.SyncState.CONTENT_URI,
                                    new android.accounts.Account(amName, accountType),
                                    contactsSyncKey);
                            LogUtils.w(LogUtils.TAG, "Set contacts key...");
                        } catch (RemoteException e) {
                            LogUtils.w(LogUtils.TAG, "Set contacts key FAILED");
                        }
                    }

                    // That's all folks!
                    LogUtils.w(LogUtils.TAG, "Account update completed.");
                } finally {
                    // Clear the incomplete flag on the provider account
                    accountValues.put(AccountColumns.FLAGS, oldFlags);
                    resolver.update(accountUri, accountValues, null, null);
                    LogUtils.w(LogUtils.TAG, "[Incomplete flag cleared]");
                }
            }
        } finally {
            c.close();
        }
    }

    private static void moveCalendarData(final ContentResolver resolver, final String name,
            final String oldType, final String newType) {
        final Uri oldCalendars = Calendars.CONTENT_URI.buildUpon()
                .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
                .appendQueryParameter(Calendars.ACCOUNT_NAME, name)
                .appendQueryParameter(Calendars.ACCOUNT_TYPE, oldType)
                .build();

        // Update this calendar to have the new account type.
        final ContentValues values = new ContentValues();
        values.put(CalendarContract.Calendars.ACCOUNT_TYPE, newType);
        resolver.update(oldCalendars, values,
                Calendars.ACCOUNT_NAME + "=? AND " + Calendars.ACCOUNT_TYPE + "=?",
                new String[] {name, oldType});
    }

    private static void moveContactsData(final ContentResolver resolver, final String name,
            final String oldType, final String newType) {
        final Uri oldContacts = RawContacts.CONTENT_URI.buildUpon()
                .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
                .appendQueryParameter(RawContacts.ACCOUNT_NAME, name)
                .appendQueryParameter(RawContacts.ACCOUNT_TYPE, oldType)
                .build();

        // Update this calendar to have the new account type.
        final ContentValues values = new ContentValues();
        values.put(CalendarContract.Calendars.ACCOUNT_TYPE, newType);
        resolver.update(oldContacts, values, null, null);
    }

    private static final Configuration sOldConfiguration = new Configuration();
    private static Map<String, EmailServiceInfo> sServiceMap = null;
    private static final Object sServiceMapLock = new Object();

    /**
     * Parse services.xml file to find our available email services
     */
    private static Map<String, EmailServiceInfo> getServiceMap(final Context context) {
        synchronized (sServiceMapLock) {
            /**
             * We cache localized strings here, so make sure to regenerate the service map if
             * the locale changes
             */
            if (sServiceMap == null) {
                sOldConfiguration.setTo(context.getResources().getConfiguration());
            }

            final int delta =
                    sOldConfiguration.updateFrom(context.getResources().getConfiguration());

            if (sServiceMap != null
                    && !Configuration.needNewResources(delta, ActivityInfo.CONFIG_LOCALE)) {
                return sServiceMap;
            }

            final ImmutableMap.Builder<String, EmailServiceInfo> builder = ImmutableMap.builder();

            try {
                final Resources res = context.getResources();
                final XmlResourceParser xml = res.getXml(R.xml.services);
                int xmlEventType;
                // walk through senders.xml file.
                while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) {
                    if (xmlEventType == XmlResourceParser.START_TAG &&
                            "emailservice".equals(xml.getName())) {
                        final EmailServiceInfo info = new EmailServiceInfo();
                        final TypedArray ta =
                                res.obtainAttributes(xml, R.styleable.EmailServiceInfo);
                        info.protocol = ta.getString(R.styleable.EmailServiceInfo_protocol);
                        info.accountType = ta.getString(R.styleable.EmailServiceInfo_accountType);
                        info.name = ta.getString(R.styleable.EmailServiceInfo_name);
                        info.hide = ta.getBoolean(R.styleable.EmailServiceInfo_hide, false);
                        final String klass =
                                ta.getString(R.styleable.EmailServiceInfo_serviceClass);
                        info.intentAction = ta.getString(R.styleable.EmailServiceInfo_intent);
                        info.intentPackage =
                                ta.getString(R.styleable.EmailServiceInfo_intentPackage);
                        info.defaultSsl =
                                ta.getBoolean(R.styleable.EmailServiceInfo_defaultSsl, false);
                        info.port = ta.getInteger(R.styleable.EmailServiceInfo_port, 0);
                        info.portSsl = ta.getInteger(R.styleable.EmailServiceInfo_portSsl, 0);
                        info.offerTls = ta.getBoolean(R.styleable.EmailServiceInfo_offerTls, false);
                        info.offerCerts =
                                ta.getBoolean(R.styleable.EmailServiceInfo_offerCerts, false);
                        info.offerOAuth =
                                ta.getBoolean(R.styleable.EmailServiceInfo_offerOAuth, false);
                        info.offerLocalDeletes =
                            ta.getBoolean(R.styleable.EmailServiceInfo_offerLocalDeletes, false);
                        info.defaultLocalDeletes =
                            ta.getInteger(R.styleable.EmailServiceInfo_defaultLocalDeletes,
                                    Account.DELETE_POLICY_ON_DELETE);
                        info.offerPrefix =
                            ta.getBoolean(R.styleable.EmailServiceInfo_offerPrefix, false);
                        info.usesSmtp = ta.getBoolean(R.styleable.EmailServiceInfo_usesSmtp, false);
                        info.usesAutodiscover =
                            ta.getBoolean(R.styleable.EmailServiceInfo_usesAutodiscover, false);
                        info.offerLookback =
                            ta.getBoolean(R.styleable.EmailServiceInfo_offerLookback, false);
                        info.defaultLookback =
                            ta.getInteger(R.styleable.EmailServiceInfo_defaultLookback,
                                    SyncWindow.SYNC_WINDOW_3_DAYS);
                        info.syncChanges =
                            ta.getBoolean(R.styleable.EmailServiceInfo_syncChanges, false);
                        info.syncContacts =
                            ta.getBoolean(R.styleable.EmailServiceInfo_syncContacts, false);
                        info.syncCalendar =
                            ta.getBoolean(R.styleable.EmailServiceInfo_syncCalendar, false);
                        info.offerAttachmentPreload =
                            ta.getBoolean(R.styleable.EmailServiceInfo_offerAttachmentPreload,
                                    false);
                        info.syncIntervalStrings =
                            ta.getTextArray(R.styleable.EmailServiceInfo_syncIntervalStrings);
                        info.syncIntervals =
                            ta.getTextArray(R.styleable.EmailServiceInfo_syncIntervals);
                        info.defaultSyncInterval =
                            ta.getInteger(R.styleable.EmailServiceInfo_defaultSyncInterval, 15);
                        info.inferPrefix = ta.getString(R.styleable.EmailServiceInfo_inferPrefix);
                        info.offerLoadMore =
                                ta.getBoolean(R.styleable.EmailServiceInfo_offerLoadMore, false);
                        info.offerMoveTo =
                                ta.getBoolean(R.styleable.EmailServiceInfo_offerMoveTo, false);
                        info.requiresSetup =
                                ta.getBoolean(R.styleable.EmailServiceInfo_requiresSetup, false);
                        info.isGmailStub =
                                ta.getBoolean(R.styleable.EmailServiceInfo_isGmailStub, false);

                        // Must have either "class" (local) or "intent" (remote)
                        if (klass != null) {
                            try {
                                // noinspection unchecked
                                info.klass = (Class<? extends Service>) Class.forName(klass);
                            } catch (ClassNotFoundException e) {
                                throw new IllegalStateException(
                                        "Class not found in service descriptor: " + klass);
                            }
                        }
                        if (info.klass == null &&
                                info.intentAction == null &&
                                !info.isGmailStub) {
                            throw new IllegalStateException(
                                    "No class or intent action specified in service descriptor");
                        }
                        if (info.klass != null && info.intentAction != null) {
                            throw new IllegalStateException(
                                    "Both class and intent action specified in service descriptor");
                        }
                        builder.put(info.protocol, info);
                    }
                }
            } catch (XmlPullParserException e) {
                // ignore
            } catch (IOException e) {
                // ignore
            }
            sServiceMap = builder.build();
            return sServiceMap;
        }
    }

    /**
     * Resolves a service name into a protocol name, or null if ambiguous
     * @param context for loading service map
     * @param accountType sync adapter service name
     * @return protocol name or null
     */
    public static @Nullable String getProtocolFromAccountType(final Context context,
            final String accountType) {
        if (TextUtils.isEmpty(accountType)) {
            return null;
        }
        final Map <String, EmailServiceInfo> serviceInfoMap = getServiceMap(context);
        String protocol = null;
        for (final EmailServiceInfo info : serviceInfoMap.values()) {
            if (TextUtils.equals(accountType, info.accountType)) {
                if (!TextUtils.isEmpty(protocol) && !TextUtils.equals(protocol, info.protocol)) {
                    // More than one protocol matches
                    return null;
                }
                protocol = info.protocol;
            }
        }
        return protocol;
    }

    private static Uri asCalendarSyncAdapter(Uri uri, String account, String accountType) {
        return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
                .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
                .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
    }

    /**
     * A no-op service that can be returned for non-existent/null protocols
     */
    class NullService implements IEmailService {
        @Override
        public IBinder asBinder() {
            return null;
        }

        @Override
        public Bundle validate(HostAuthCompat hostauth) throws RemoteException {
            return null;
        }

        @Override
        public void loadAttachment(final IEmailServiceCallback cb, final long accountId,
                final long attachmentId, final boolean background) throws RemoteException {
        }

        @Override
        public void updateFolderList(long accountId) throws RemoteException {}

        @Override
        public void setLogging(int flags) throws RemoteException {
        }

        @Override
        public Bundle autoDiscover(String userName, String password) throws RemoteException {
            return null;
        }

        @Override
        public void sendMeetingResponse(long messageId, int response) throws RemoteException {
        }

        @Override
        public void deleteExternalAccountPIMData(final String emailAddress) throws RemoteException {
        }

        @Override
        public int searchMessages(long accountId, SearchParams params, long destMailboxId)
                throws RemoteException {
            return 0;
        }

        @Override
        public void sendMail(long accountId) throws RemoteException {
        }

        @Override
        public void pushModify(long accountId) throws RemoteException {
        }

        @Override
        public int sync(final long accountId, final Bundle syncExtras) {
            return EmailServiceStatus.SUCCESS;
        }

        public int getApiVersion() {
            return EmailServiceVersion.CURRENT;
        }
    }

    public static void setComponentStatus(final Context context, Class<?> clazz, boolean enabled) {
        final ComponentName c = new ComponentName(context, clazz.getName());
        context.getPackageManager().setComponentEnabledSetting(c,
                enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
                        : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
                PackageManager.DONT_KILL_APP);
    }

    /**
     * This is a helper function that enables the proper Exchange component and disables
     * the other Exchange component ensuring that only one is enabled at a time.
     */
    public static void enableExchangeComponent(final Context context) {
        if (VendorPolicyLoader.getInstance(context).useAlternateExchangeStrings()) {
            LogUtils.d(LogUtils.TAG, "Enabling alternate EAS authenticator");
            setComponentStatus(context, EasAuthenticatorServiceAlternate.class, true);
            setComponentStatus(context, EasAuthenticatorService.class, false);
        } else {
            LogUtils.d(LogUtils.TAG, "Enabling EAS authenticator");
            setComponentStatus(context, EasAuthenticatorService.class, true);
            setComponentStatus(context,
                    EasAuthenticatorServiceAlternate.class, false);
        }
    }

    public static void disableExchangeComponents(final Context context) {
        LogUtils.d(LogUtils.TAG, "Disabling EAS authenticators");
        setComponentStatus(context, EasAuthenticatorServiceAlternate.class, false);
        setComponentStatus(context, EasAuthenticatorService.class, false);
    }

}