EmailServiceUtils.java revision 70edcf05387df33f4761b766add6b80999e425e9
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.AccountManagerFuture;
21import android.accounts.AuthenticatorException;
22import android.accounts.OperationCanceledException;
23import android.app.Service;
24import android.content.ContentResolver;
25import android.content.ContentUris;
26import android.content.ContentValues;
27import android.content.Context;
28import android.content.Intent;
29import android.content.res.Resources;
30import android.content.res.TypedArray;
31import android.content.res.XmlResourceParser;
32import android.database.Cursor;
33import android.net.Uri;
34import android.os.Bundle;
35import android.os.IBinder;
36import android.os.RemoteException;
37import android.provider.CalendarContract;
38import android.provider.ContactsContract;
39import android.util.Log;
40
41import com.android.email.R;
42import com.android.emailcommon.Api;
43import com.android.emailcommon.Logging;
44import com.android.emailcommon.provider.Account;
45import com.android.emailcommon.provider.EmailContent;
46import com.android.emailcommon.provider.HostAuth;
47import com.android.emailcommon.provider.EmailContent.AccountColumns;
48import com.android.emailcommon.service.EmailServiceProxy;
49import com.android.emailcommon.service.IEmailService;
50import com.android.emailcommon.service.IEmailServiceCallback;
51import com.android.emailcommon.service.SearchParams;
52import com.android.emailcommon.service.SyncWindow;
53
54import org.xmlpull.v1.XmlPullParserException;
55
56import java.io.IOException;
57import java.util.ArrayList;
58import java.util.List;
59
60/**
61 * Utility functions for EmailService support.
62 */
63public class EmailServiceUtils {
64    private static final ArrayList<EmailServiceInfo> sServiceList =
65            new ArrayList<EmailServiceInfo>();
66
67    /**
68     * Starts an EmailService by protocol
69     */
70    public static void startService(Context context, String protocol) {
71        EmailServiceInfo info = getServiceInfo(context, protocol);
72        if (info != null && info.intentAction != null) {
73            context.startService(new Intent(info.intentAction));
74        }
75    }
76
77    /**
78     * Starts all remote services
79     */
80    public static void startRemoteServices(Context context) {
81        for (EmailServiceInfo info: getServiceInfoList(context)) {
82            if (info.intentAction != null) {
83                context.startService(new Intent(info.intentAction));
84            }
85        }
86    }
87
88    /**
89     * Returns whether or not remote services are present on device
90     */
91    public static boolean areRemoteServicesInstalled(Context context) {
92        for (EmailServiceInfo info: getServiceInfoList(context)) {
93            if (info.intentAction != null) {
94                return true;
95            }
96        }
97        return false;
98    }
99
100    /**
101     * Starts all remote services
102     */
103    public static void setRemoteServicesLogging(Context context, int debugBits) {
104        for (EmailServiceInfo info: getServiceInfoList(context)) {
105            if (info.intentAction != null) {
106                EmailServiceProxy service =
107                        EmailServiceUtils.getService(context, null, info.protocol);
108                if (service != null) {
109                    try {
110                        service.setLogging(debugBits);
111                    } catch (RemoteException e) {
112                        // Move along, nothing to see
113                    }
114                }
115            }
116        }
117    }
118
119    /**
120     * Determine if the EmailService is available
121     */
122    public static boolean isServiceAvailable(Context context, String protocol) {
123        EmailServiceInfo info = getServiceInfo(context, protocol);
124        if (info == null) return false;
125        if (info.klass != null) return true;
126        return new EmailServiceProxy(context, info.intentAction, null).test();
127    }
128
129    /**
130     * For a given account id, return a service proxy if applicable, or null.
131     *
132     * @param accountId the message of interest
133     * @result service proxy, or null if n/a
134     */
135    public static EmailServiceProxy getServiceForAccount(Context context,
136            IEmailServiceCallback callback, long accountId) {
137        return getService(context, callback, Account.getProtocol(context, accountId));
138    }
139
140    /**
141     * Holder of service information (currently just name and class/intent); if there is a class
142     * member, this is a (local, i.e. same process) service; otherwise, this is a remote service
143     */
144    public static class EmailServiceInfo {
145        public String protocol;
146        public String name;
147        public String accountType;
148        Class<? extends Service> klass;
149        String intentAction;
150        public int port;
151        public int portSsl;
152        public boolean defaultSsl;
153        public boolean offerTls;
154        public boolean offerCerts;
155        public boolean usesSmtp;
156        public boolean offerLocalDeletes;
157        public int defaultLocalDeletes;
158        public boolean offerPrefix;
159        public boolean usesAutodiscover;
160        public boolean offerLookback;
161        public int defaultLookback;
162        public boolean syncChanges;
163        public boolean syncContacts;
164        public boolean syncCalendar;
165        public boolean offerAttachmentPreload;
166        public CharSequence[] syncIntervalStrings;
167        public CharSequence[] syncIntervals;
168        public int defaultSyncInterval;
169        public String inferPrefix;
170
171        public String toString() {
172            StringBuilder sb = new StringBuilder("Protocol: ");
173            sb.append(protocol);
174            sb.append(", ");
175            sb.append(klass != null ? "Local" : "Remote");
176            return sb.toString();
177        }
178    }
179
180    public static EmailServiceProxy getService(Context context, IEmailServiceCallback callback,
181            String protocol) {
182        EmailServiceInfo info = null;
183        // Handle the degenerate case here (account might have been deleted)
184        if (protocol != null) {
185            info = getServiceInfo(context, protocol);
186        }
187        if (info == null) {
188            Log.w(Logging.LOG_TAG, "Returning NullService for " + protocol);
189            return new EmailServiceProxy(context, NullService.class, null);
190        } else if (info.klass != null) {
191            return new EmailServiceProxy(context, info.klass, callback);
192        } else {
193            return new EmailServiceProxy(context, info.intentAction, callback);
194        }
195    }
196
197    public static EmailServiceInfo getServiceInfo(Context context, String protocol) {
198        if (sServiceList.isEmpty()) {
199            findServices(context);
200        }
201        for (EmailServiceInfo info: sServiceList) {
202            if (info.protocol.equals(protocol)) {
203                return info;
204            }
205        }
206        return null;
207    }
208
209    public static List<EmailServiceInfo> getServiceInfoList(Context context) {
210        synchronized(sServiceList) {
211            if (sServiceList.isEmpty()) {
212                findServices(context);
213            }
214            return sServiceList;
215        }
216    }
217
218    private static void finishAccountManagerBlocker(AccountManagerFuture<?> future) {
219        try {
220            // Note: All of the potential errors are simply logged
221            // here, as there is nothing to actually do about them.
222            future.getResult();
223        } catch (OperationCanceledException e) {
224            Log.w(Logging.LOG_TAG, e.toString());
225        } catch (AuthenticatorException e) {
226            Log.w(Logging.LOG_TAG, e.toString());
227        } catch (IOException e) {
228            Log.w(Logging.LOG_TAG, e.toString());
229        }
230    }
231
232    /**
233     * "Change" the account manager type of the account; this entails deleting the account
234     * and adding a new one.  We can't call into AccountManager on the UI thread, but we might
235     * well be on it (currently no clean way of guaranteeing that we're not).
236     *
237     * @param context the caller's context
238     * @param amAccount the AccountManager account we're changing
239     * @param newType the new AccountManager type for this account
240     * @param newProtocol the protocol now being used
241     */
242    private static void updateAccountManagerType(final Context context,
243            final android.accounts.Account amAccount, final String newType,
244            final String newProtocol) {
245        // STOPSHIP There must be a better way
246        Thread amThread = new Thread(new Runnable() {
247           @Override
248            public void run() {
249               updateAccountManagerTypeImpl(context, amAccount, newType, newProtocol);
250            }});
251        amThread.start();
252    }
253
254    private static void updateAccountManagerTypeImpl(Context context,
255            android.accounts.Account amAccount, String newType, String newProtocol) {
256        ContentResolver resolver = context.getContentResolver();
257        Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION,
258                AccountColumns.EMAIL_ADDRESS + "=?", new String[] { amAccount.name }, null);
259        // That's odd, isn't it?
260        if (c == null) return;
261        try {
262            if (c.moveToNext()) {
263                Log.w(Logging.LOG_TAG, "Converting " + amAccount.name + " to " + newProtocol);
264                // Get the EmailProvider Account/HostAuth
265                Account account = new Account();
266                account.restore(c);
267                HostAuth hostAuth =
268                        HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
269                if (hostAuth == null) return;
270
271                ContentValues accountValues = new ContentValues();
272                int oldFlags = account.mFlags;
273
274                // Mark the provider account incomplete so it can't get reconciled away
275                account.mFlags |= Account.FLAGS_INCOMPLETE;
276                accountValues.put(AccountColumns.FLAGS, account.mFlags);
277                Uri accountUri = ContentUris.withAppendedId(Account.CONTENT_URI, account.mId);
278                resolver.update(accountUri, accountValues, null, null);
279
280                // Change the HostAuth to reference the new protocol; this has to be done before
281                // trying to create the AccountManager account (below)
282                ContentValues hostValues = new ContentValues();
283                hostValues.put(HostAuth.PROTOCOL, newProtocol);
284                resolver.update(ContentUris.withAppendedId(HostAuth.CONTENT_URI, hostAuth.mId),
285                        hostValues, null, null);
286
287                try {
288                    // Get current settings for the existing AccountManager account
289                    boolean email = ContentResolver.getSyncAutomatically(amAccount,
290                            EmailContent.AUTHORITY);
291                    boolean contacts = ContentResolver.getSyncAutomatically(amAccount,
292                            ContactsContract.AUTHORITY);
293                    boolean calendar = ContentResolver.getSyncAutomatically(amAccount,
294                            CalendarContract.AUTHORITY);
295
296                    // Delete the AccountManager account
297                    AccountManagerFuture<?> amFuture = AccountManager.get(context)
298                            .removeAccount(amAccount, null, null);
299                    finishAccountManagerBlocker(amFuture);
300
301                    // Set up a new AccountManager account with new type and old settings
302                    amFuture = MailService.setupAccountManagerAccount(context, account, email,
303                            calendar, contacts, null);
304                    finishAccountManagerBlocker(amFuture);
305                    Log.w(Logging.LOG_TAG, "Conversion complete!");
306                } finally {
307                    // Clear the incomplete flag on the provider account
308                    accountValues.put(AccountColumns.FLAGS, oldFlags);
309                    resolver.update(accountUri, accountValues, null, null);
310                }
311            }
312        } finally {
313            c.close();
314        }
315    }
316
317    /**
318     * Parse services.xml file to find our available email services
319     */
320    @SuppressWarnings("unchecked")
321    private static synchronized void findServices(Context context) {
322        try {
323            Resources res = context.getResources();
324            XmlResourceParser xml = res.getXml(R.xml.services);
325            int xmlEventType;
326            // walk through senders.xml file.
327            while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) {
328                if (xmlEventType == XmlResourceParser.START_TAG &&
329                        "emailservice".equals(xml.getName())) {
330                    EmailServiceInfo info = new EmailServiceInfo();
331                    TypedArray ta = res.obtainAttributes(xml, R.styleable.EmailServiceInfo);
332                    info.protocol = ta.getString(R.styleable.EmailServiceInfo_protocol);
333                    info.accountType = ta.getString(R.styleable.EmailServiceInfo_accountType);
334                    // Handle upgrade of one protocol to another (e.g. imap to imap2)
335                    String newProtocol = ta.getString(R.styleable.EmailServiceInfo_replaceWith);
336                    if (newProtocol != null) {
337                        EmailServiceInfo newInfo = getServiceInfo(context, newProtocol);
338                        if (newInfo == null) {
339                            throw new IllegalStateException(
340                                    "Replacement service not found: " + newProtocol);
341                        }
342                        AccountManager am = AccountManager.get(context);
343                        android.accounts.Account[] amAccounts =
344                                am.getAccountsByType(info.accountType);
345                        for (android.accounts.Account amAccount: amAccounts) {
346                            updateAccountManagerType(context, amAccount, newInfo.accountType,
347                                    newProtocol);
348                        }
349                        continue;
350                    }
351                    info.name = ta.getString(R.styleable.EmailServiceInfo_name);
352                    String klass = ta.getString(R.styleable.EmailServiceInfo_serviceClass);
353                    info.intentAction = ta.getString(R.styleable.EmailServiceInfo_intent);
354                    info.defaultSsl = ta.getBoolean(R.styleable.EmailServiceInfo_defaultSsl, false);
355                    info.port = ta.getInteger(R.styleable.EmailServiceInfo_port, 0);
356                    info.portSsl = ta.getInteger(R.styleable.EmailServiceInfo_portSsl, 0);
357                    info.offerTls = ta.getBoolean(R.styleable.EmailServiceInfo_offerTls, false);
358                    info.offerCerts = ta.getBoolean(R.styleable.EmailServiceInfo_offerCerts, false);
359                    info.offerLocalDeletes =
360                        ta.getBoolean(R.styleable.EmailServiceInfo_offerLocalDeletes, false);
361                    info.defaultLocalDeletes =
362                        ta.getInteger(R.styleable.EmailServiceInfo_defaultLocalDeletes,
363                                Account.DELETE_POLICY_ON_DELETE);
364                    info.offerPrefix =
365                        ta.getBoolean(R.styleable.EmailServiceInfo_offerPrefix, false);
366                    info.usesSmtp = ta.getBoolean(R.styleable.EmailServiceInfo_usesSmtp, false);
367                    info.usesAutodiscover =
368                        ta.getBoolean(R.styleable.EmailServiceInfo_usesAutodiscover, false);
369                    info.offerLookback =
370                        ta.getBoolean(R.styleable.EmailServiceInfo_offerLookback, false);
371                    info.defaultLookback =
372                        ta.getInteger(R.styleable.EmailServiceInfo_defaultLookback,
373                                SyncWindow.SYNC_WINDOW_3_DAYS);
374                    info.syncChanges =
375                        ta.getBoolean(R.styleable.EmailServiceInfo_syncChanges, false);
376                    info.syncContacts =
377                        ta.getBoolean(R.styleable.EmailServiceInfo_syncContacts, false);
378                    info.syncCalendar =
379                        ta.getBoolean(R.styleable.EmailServiceInfo_syncCalendar, false);
380                    info.offerAttachmentPreload =
381                        ta.getBoolean(R.styleable.EmailServiceInfo_offerAttachmentPreload, false);
382                    info.syncIntervalStrings =
383                        ta.getTextArray(R.styleable.EmailServiceInfo_syncIntervalStrings);
384                    info.syncIntervals =
385                        ta.getTextArray(R.styleable.EmailServiceInfo_syncIntervals);
386                    info.defaultSyncInterval =
387                        ta.getInteger(R.styleable.EmailServiceInfo_defaultSyncInterval, 15);
388                    info.inferPrefix = ta.getString(R.styleable.EmailServiceInfo_inferPrefix);
389
390                    // Must have either "class" (local) or "intent" (remote)
391                    if (klass != null) {
392                        try {
393                            info.klass = (Class<? extends Service>) Class.forName(klass);
394                        } catch (ClassNotFoundException e) {
395                            throw new IllegalStateException(
396                                    "Class not found in service descriptor: " + klass);
397                        }
398                    }
399                    if (info.klass == null && info.intentAction == null) {
400                        throw new IllegalStateException(
401                                "No class or intent action specified in service descriptor");
402                    }
403                    if (info.klass != null && info.intentAction != null) {
404                        throw new IllegalStateException(
405                                "Both class and intent action specified in service descriptor");
406                    }
407                    sServiceList.add(info);
408                }
409            }
410        } catch (XmlPullParserException e) {
411            // ignore
412        } catch (IOException e) {
413            // ignore
414        }
415    }
416
417    /**
418     * A no-op service that can be returned for non-existent/null protocols
419     */
420    class NullService implements IEmailService {
421        @Override
422        public IBinder asBinder() {
423            return null;
424        }
425
426        @Override
427        public Bundle validate(HostAuth hostauth) throws RemoteException {
428            return null;
429        }
430
431        @Override
432        public void startSync(long mailboxId, boolean userRequest) throws RemoteException {
433        }
434
435        @Override
436        public void stopSync(long mailboxId) throws RemoteException {
437        }
438
439        @Override
440        public void loadMore(long messageId) throws RemoteException {
441        }
442
443        @Override
444        public void loadAttachment(long attachmentId, boolean background) throws RemoteException {
445        }
446
447        @Override
448        public void updateFolderList(long accountId) throws RemoteException {
449        }
450
451        @Override
452        public boolean createFolder(long accountId, String name) throws RemoteException {
453            return false;
454        }
455
456        @Override
457        public boolean deleteFolder(long accountId, String name) throws RemoteException {
458            return false;
459        }
460
461        @Override
462        public boolean renameFolder(long accountId, String oldName, String newName)
463                throws RemoteException {
464            return false;
465        }
466
467        @Override
468        public void setCallback(IEmailServiceCallback cb) throws RemoteException {
469        }
470
471        @Override
472        public void setLogging(int on) throws RemoteException {
473        }
474
475        @Override
476        public void hostChanged(long accountId) throws RemoteException {
477        }
478
479        @Override
480        public Bundle autoDiscover(String userName, String password) throws RemoteException {
481            return null;
482        }
483
484        @Override
485        public void sendMeetingResponse(long messageId, int response) throws RemoteException {
486        }
487
488        @Override
489        public void deleteAccountPIMData(long accountId) throws RemoteException {
490        }
491
492        @Override
493        public int getApiLevel() throws RemoteException {
494            return Api.LEVEL;
495        }
496
497        @Override
498        public int searchMessages(long accountId, SearchParams params, long destMailboxId)
499                throws RemoteException {
500            return 0;
501        }
502
503        @Override
504        public void sendMail(long accountId) throws RemoteException {
505        }
506
507        @Override
508        public int getCapabilities(Account acct) throws RemoteException {
509            return 0;
510        }
511    }
512}
513