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