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
547            try {
548                final Resources res = context.getResources();
549                final XmlResourceParser xml = res.getXml(R.xml.services);
550                int xmlEventType;
551                // walk through senders.xml file.
552                while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) {
553                    if (xmlEventType == XmlResourceParser.START_TAG &&
554                            "emailservice".equals(xml.getName())) {
555                        final EmailServiceInfo info = new EmailServiceInfo();
556                        final TypedArray ta =
557                                res.obtainAttributes(xml, R.styleable.EmailServiceInfo);
558                        info.protocol = ta.getString(R.styleable.EmailServiceInfo_protocol);
559                        info.accountType = ta.getString(R.styleable.EmailServiceInfo_accountType);
560                        info.name = ta.getString(R.styleable.EmailServiceInfo_name);
561                        info.hide = ta.getBoolean(R.styleable.EmailServiceInfo_hide, false);
562                        final String klass =
563                                ta.getString(R.styleable.EmailServiceInfo_serviceClass);
564                        info.intentAction = ta.getString(R.styleable.EmailServiceInfo_intent);
565                        info.intentPackage =
566                                ta.getString(R.styleable.EmailServiceInfo_intentPackage);
567                        info.defaultSsl =
568                                ta.getBoolean(R.styleable.EmailServiceInfo_defaultSsl, false);
569                        info.port = ta.getInteger(R.styleable.EmailServiceInfo_port, 0);
570                        info.portSsl = ta.getInteger(R.styleable.EmailServiceInfo_portSsl, 0);
571                        info.offerTls = ta.getBoolean(R.styleable.EmailServiceInfo_offerTls, false);
572                        info.offerCerts =
573                                ta.getBoolean(R.styleable.EmailServiceInfo_offerCerts, false);
574                        info.offerOAuth =
575                                ta.getBoolean(R.styleable.EmailServiceInfo_offerOAuth, false);
576                        info.offerLocalDeletes =
577                            ta.getBoolean(R.styleable.EmailServiceInfo_offerLocalDeletes, false);
578                        info.defaultLocalDeletes =
579                            ta.getInteger(R.styleable.EmailServiceInfo_defaultLocalDeletes,
580                                    Account.DELETE_POLICY_ON_DELETE);
581                        info.offerPrefix =
582                            ta.getBoolean(R.styleable.EmailServiceInfo_offerPrefix, false);
583                        info.usesSmtp = ta.getBoolean(R.styleable.EmailServiceInfo_usesSmtp, false);
584                        info.usesAutodiscover =
585                            ta.getBoolean(R.styleable.EmailServiceInfo_usesAutodiscover, false);
586                        info.offerLookback =
587                            ta.getBoolean(R.styleable.EmailServiceInfo_offerLookback, false);
588                        info.defaultLookback =
589                            ta.getInteger(R.styleable.EmailServiceInfo_defaultLookback,
590                                    SyncWindow.SYNC_WINDOW_3_DAYS);
591                        info.syncChanges =
592                            ta.getBoolean(R.styleable.EmailServiceInfo_syncChanges, false);
593                        info.syncContacts =
594                            ta.getBoolean(R.styleable.EmailServiceInfo_syncContacts, false);
595                        info.syncCalendar =
596                            ta.getBoolean(R.styleable.EmailServiceInfo_syncCalendar, false);
597                        info.offerAttachmentPreload =
598                            ta.getBoolean(R.styleable.EmailServiceInfo_offerAttachmentPreload,
599                                    false);
600                        info.syncIntervalStrings =
601                            ta.getTextArray(R.styleable.EmailServiceInfo_syncIntervalStrings);
602                        info.syncIntervals =
603                            ta.getTextArray(R.styleable.EmailServiceInfo_syncIntervals);
604                        info.defaultSyncInterval =
605                            ta.getInteger(R.styleable.EmailServiceInfo_defaultSyncInterval, 15);
606                        info.inferPrefix = ta.getString(R.styleable.EmailServiceInfo_inferPrefix);
607                        info.offerLoadMore =
608                                ta.getBoolean(R.styleable.EmailServiceInfo_offerLoadMore, false);
609                        info.offerMoveTo =
610                                ta.getBoolean(R.styleable.EmailServiceInfo_offerMoveTo, false);
611                        info.requiresSetup =
612                                ta.getBoolean(R.styleable.EmailServiceInfo_requiresSetup, false);
613                        info.isGmailStub =
614                                ta.getBoolean(R.styleable.EmailServiceInfo_isGmailStub, false);
615
616                        // Must have either "class" (local) or "intent" (remote)
617                        if (klass != null) {
618                            try {
619                                // noinspection unchecked
620                                info.klass = (Class<? extends Service>) Class.forName(klass);
621                            } catch (ClassNotFoundException e) {
622                                throw new IllegalStateException(
623                                        "Class not found in service descriptor: " + klass);
624                            }
625                        }
626                        if (info.klass == null &&
627                                info.intentAction == null &&
628                                !info.isGmailStub) {
629                            throw new IllegalStateException(
630                                    "No class or intent action specified in service descriptor");
631                        }
632                        if (info.klass != null && info.intentAction != null) {
633                            throw new IllegalStateException(
634                                    "Both class and intent action specified in service descriptor");
635                        }
636                        builder.put(info.protocol, info);
637                    }
638                }
639            } catch (XmlPullParserException e) {
640                // ignore
641            } catch (IOException e) {
642                // ignore
643            }
644            sServiceMap = builder.build();
645            return sServiceMap;
646        }
647    }
648
649    /**
650     * Resolves a service name into a protocol name, or null if ambiguous
651     * @param context for loading service map
652     * @param accountType sync adapter service name
653     * @return protocol name or null
654     */
655    public static @Nullable String getProtocolFromAccountType(final Context context,
656            final String accountType) {
657        if (TextUtils.isEmpty(accountType)) {
658            return null;
659        }
660        final Map <String, EmailServiceInfo> serviceInfoMap = getServiceMap(context);
661        String protocol = null;
662        for (final EmailServiceInfo info : serviceInfoMap.values()) {
663            if (TextUtils.equals(accountType, info.accountType)) {
664                if (!TextUtils.isEmpty(protocol) && !TextUtils.equals(protocol, info.protocol)) {
665                    // More than one protocol matches
666                    return null;
667                }
668                protocol = info.protocol;
669            }
670        }
671        return protocol;
672    }
673
674    private static Uri asCalendarSyncAdapter(Uri uri, String account, String accountType) {
675        return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
676                .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
677                .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
678    }
679
680    /**
681     * A no-op service that can be returned for non-existent/null protocols
682     */
683    class NullService implements IEmailService {
684        @Override
685        public IBinder asBinder() {
686            return null;
687        }
688
689        @Override
690        public Bundle validate(HostAuthCompat hostauth) throws RemoteException {
691            return null;
692        }
693
694        @Override
695        public void loadAttachment(final IEmailServiceCallback cb, final long accountId,
696                final long attachmentId, final boolean background) throws RemoteException {
697        }
698
699        @Override
700        public void updateFolderList(long accountId) throws RemoteException {}
701
702        @Override
703        public void setLogging(int flags) throws RemoteException {
704        }
705
706        @Override
707        public Bundle autoDiscover(String userName, String password) throws RemoteException {
708            return null;
709        }
710
711        @Override
712        public void sendMeetingResponse(long messageId, int response) throws RemoteException {
713        }
714
715        @Override
716        public void deleteExternalAccountPIMData(final String emailAddress) throws RemoteException {
717        }
718
719        @Override
720        public int searchMessages(long accountId, SearchParams params, long destMailboxId)
721                throws RemoteException {
722            return 0;
723        }
724
725        @Override
726        public void sendMail(long accountId) throws RemoteException {
727        }
728
729        @Override
730        public void pushModify(long accountId) throws RemoteException {
731        }
732
733        @Override
734        public int sync(final long accountId, final Bundle syncExtras) {
735            return EmailServiceStatus.SUCCESS;
736        }
737
738        public int getApiVersion() {
739            return EmailServiceVersion.CURRENT;
740        }
741    }
742
743    public static void setComponentStatus(final Context context, Class<?> clazz, boolean enabled) {
744        final ComponentName c = new ComponentName(context, clazz.getName());
745        context.getPackageManager().setComponentEnabledSetting(c,
746                enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
747                        : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
748                PackageManager.DONT_KILL_APP);
749    }
750
751    /**
752     * This is a helper function that enables the proper Exchange component and disables
753     * the other Exchange component ensuring that only one is enabled at a time.
754     */
755    public static void enableExchangeComponent(final Context context) {
756        if (VendorPolicyLoader.getInstance(context).useAlternateExchangeStrings()) {
757            LogUtils.d(LogUtils.TAG, "Enabling alternate EAS authenticator");
758            setComponentStatus(context, EasAuthenticatorServiceAlternate.class, true);
759            setComponentStatus(context, EasAuthenticatorService.class, false);
760        } else {
761            LogUtils.d(LogUtils.TAG, "Enabling EAS authenticator");
762            setComponentStatus(context, EasAuthenticatorService.class, true);
763            setComponentStatus(context,
764                    EasAuthenticatorServiceAlternate.class, false);
765        }
766    }
767
768    public static void disableExchangeComponents(final Context context) {
769        LogUtils.d(LogUtils.TAG, "Disabling EAS authenticators");
770        setComponentStatus(context, EasAuthenticatorServiceAlternate.class, false);
771        setComponentStatus(context, EasAuthenticatorService.class, false);
772    }
773
774}
775