/* * 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.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.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.text.TextUtils; import com.android.email.R; 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.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 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; @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 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 setupAccountManagerAccount(final Context context, final Account account, final boolean email, final boolean calendar, final boolean contacts, final AccountManagerCallback callback) { final Bundle options = new Bundle(5); final HostAuth hostAuthRecv = HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv); if (hostAuthRecv == null) { return null; } // Set up username/password options.putString(EasAuthenticatorService.OPTIONS_USERNAME, account.mEmailAddress); options.putString(EasAuthenticatorService.OPTIONS_PASSWORD, hostAuthRecv.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, hostAuthRecv.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 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 sServiceMap = null; private static final Object sServiceMapLock = new Object(); /** * Parse services.xml file to find our available email services */ private static Map 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 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); // Must have either "class" (local) or "intent" (remote) if (klass != null) { try { // noinspection unchecked info.klass = (Class) Class.forName(klass); } catch (ClassNotFoundException e) { throw new IllegalStateException( "Class not found in service descriptor: " + klass); } } if (info.klass == null && info.intentAction == null) { 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 String getProtocolFromAccountType(final Context context, final String accountType) { final Map 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(HostAuth 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 on) 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 deleteAccountPIMData(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 void sync(final long accountId, final boolean updateFolderList, final int mailboxType, final long[] folders) { } } }