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
17package com.android.email.service;
18
19import android.accounts.AccountManager;
20import android.accounts.AccountManagerCallback;
21import android.accounts.AccountManagerFuture;
22import android.accounts.AuthenticatorException;
23import android.accounts.OperationCanceledException;
24import android.app.Service;
25import android.content.ComponentName;
26import android.content.ContentProviderClient;
27import android.content.ContentResolver;
28import android.content.ContentUris;
29import android.content.ContentValues;
30import android.content.Context;
31import android.content.Intent;
32import android.content.pm.ActivityInfo;
33import android.content.pm.PackageManager;
34import android.content.res.Configuration;
35import android.content.res.Resources;
36import android.content.res.TypedArray;
37import android.content.res.XmlResourceParser;
38import android.database.Cursor;
39import android.net.Uri;
40import android.os.Bundle;
41import android.os.IBinder;
42import android.os.RemoteException;
43import android.provider.CalendarContract;
44import android.provider.CalendarContract.Calendars;
45import android.provider.CalendarContract.SyncState;
46import android.provider.ContactsContract;
47import android.provider.ContactsContract.RawContacts;
48import android.provider.SyncStateContract;
49import android.support.annotation.Nullable;
50import android.text.TextUtils;
51
52import com.android.email.R;
53import com.android.emailcommon.VendorPolicyLoader;
54import com.android.emailcommon.provider.Account;
55import com.android.emailcommon.provider.EmailContent;
56import com.android.emailcommon.provider.EmailContent.AccountColumns;
57import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
58import com.android.emailcommon.provider.HostAuth;
59import com.android.emailcommon.service.EmailServiceProxy;
60import com.android.emailcommon.service.EmailServiceStatus;
61import com.android.emailcommon.service.EmailServiceVersion;
62import com.android.emailcommon.service.HostAuthCompat;
63import com.android.emailcommon.service.IEmailService;
64import com.android.emailcommon.service.IEmailServiceCallback;
65import com.android.emailcommon.service.SearchParams;
66import com.android.emailcommon.service.ServiceProxy;
67import com.android.emailcommon.service.SyncWindow;
68import com.android.mail.utils.LogUtils;
69import com.google.common.collect.ImmutableMap;
70
71import org.xmlpull.v1.XmlPullParserException;
72
73import java.io.IOException;
74import java.util.Collection;
75import java.util.Map;
76
77/**
78 * Utility functions for EmailService support.
79 */
80public 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     */
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     */
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     */
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     */
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     */
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     */
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
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     */
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
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
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
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
252    public static EmailServiceInfo getServiceInfoForAccount(Context context, long accountId) {
253        String protocol = Account.getProtocol(context, accountId);
254        return getServiceInfo(context, protocol);
255    }
256
257    public static EmailServiceInfo getServiceInfo(Context context, String protocol) {
258        return getServiceMap(context).get(protocol);
259    }
260
261    public static Collection<EmailServiceInfo> getServiceInfoList(Context context) {
262        return getServiceMap(context).values();
263    }
264
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     */
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     */
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
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
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
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     */
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     */
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
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
691        public IBinder asBinder() {
692            return null;
693        }
694
695        @Override
696        public Bundle validate(HostAuthCompat hostauth) throws RemoteException {
697            return null;
698        }
699
700        @Override
701        public void loadAttachment(final IEmailServiceCallback cb, final long accountId,
702                final long attachmentId, final boolean background) throws RemoteException {
703        }
704
705        @Override
706        public void updateFolderList(long accountId) throws RemoteException {}
707
708        @Override
709        public void setLogging(int flags) throws RemoteException {
710        }
711
712        @Override
713        public Bundle autoDiscover(String userName, String password) throws RemoteException {
714            return null;
715        }
716
717        @Override
718        public void sendMeetingResponse(long messageId, int response) throws RemoteException {
719        }
720
721        @Override
722        public void deleteExternalAccountPIMData(final String emailAddress) throws RemoteException {
723        }
724
725        @Override
726        public int searchMessages(long accountId, SearchParams params, long destMailboxId)
727                throws RemoteException {
728            return 0;
729        }
730
731        @Override
732        public void sendMail(long accountId) throws RemoteException {
733        }
734
735        @Override
736        public void pushModify(long accountId) throws RemoteException {
737        }
738
739        @Override
740        public int sync(final long accountId, final Bundle syncExtras) {
741            return EmailServiceStatus.SUCCESS;
742        }
743
744        public int getApiVersion() {
745            return EmailServiceVersion.CURRENT;
746        }
747    }
748
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     */
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
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