EmailServiceUtils.java revision e714bb9d153cfe13a7f0932e7d67ea08fa5a1d98
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.ComponentName; 25import android.content.ContentProviderClient; 26import android.content.ContentResolver; 27import android.content.ContentUris; 28import android.content.ContentValues; 29import android.content.Context; 30import android.content.Intent; 31import android.content.pm.PackageManager; 32import android.content.res.Resources; 33import android.content.res.TypedArray; 34import android.content.res.XmlResourceParser; 35import android.database.Cursor; 36import android.net.Uri; 37import android.os.AsyncTask; 38import android.os.Bundle; 39import android.os.Debug; 40import android.os.IBinder; 41import android.os.RemoteException; 42import android.provider.CalendarContract; 43import android.provider.CalendarContract.Calendars; 44import android.provider.CalendarContract.SyncState; 45import android.provider.ContactsContract; 46import android.provider.SyncStateContract; 47import android.util.Log; 48 49import com.android.email.R; 50import com.android.emailcommon.Api; 51import com.android.emailcommon.Logging; 52import com.android.emailcommon.provider.Account; 53import com.android.emailcommon.provider.EmailContent; 54import com.android.emailcommon.provider.EmailContent.AccountColumns; 55import com.android.emailcommon.provider.HostAuth; 56import com.android.emailcommon.service.EmailServiceProxy; 57import com.android.emailcommon.service.IEmailService; 58import com.android.emailcommon.service.IEmailServiceCallback; 59import com.android.emailcommon.service.SearchParams; 60import com.android.emailcommon.service.SyncWindow; 61 62import org.xmlpull.v1.XmlPullParserException; 63 64import java.io.IOException; 65import java.util.ArrayList; 66import java.util.List; 67 68/** 69 * Utility functions for EmailService support. 70 */ 71public class EmailServiceUtils { 72 private static final ArrayList<EmailServiceInfo> sServiceList = 73 new ArrayList<EmailServiceInfo>(); 74 75 /** 76 * Starts an EmailService by protocol 77 */ 78 public static void startService(Context context, String protocol) { 79 EmailServiceInfo info = getServiceInfo(context, protocol); 80 if (info != null && info.intentAction != null) { 81 context.startService(new Intent(info.intentAction)); 82 } 83 } 84 85 /** 86 * Starts all remote services 87 */ 88 public static void startRemoteServices(Context context) { 89 for (EmailServiceInfo info: getServiceInfoList(context)) { 90 if (info.intentAction != null) { 91 context.startService(new Intent(info.intentAction)); 92 } 93 } 94 } 95 96 /** 97 * Returns whether or not remote services are present on device 98 */ 99 public static boolean areRemoteServicesInstalled(Context context) { 100 for (EmailServiceInfo info: getServiceInfoList(context)) { 101 if (info.intentAction != null) { 102 return true; 103 } 104 } 105 return false; 106 } 107 108 /** 109 * Starts all remote services 110 */ 111 public static void setRemoteServicesLogging(Context context, int debugBits) { 112 for (EmailServiceInfo info: getServiceInfoList(context)) { 113 if (info.intentAction != null) { 114 EmailServiceProxy service = 115 EmailServiceUtils.getService(context, null, info.protocol); 116 if (service != null) { 117 try { 118 service.setLogging(debugBits); 119 } catch (RemoteException e) { 120 // Move along, nothing to see 121 } 122 } 123 } 124 } 125 } 126 127 /** 128 * Determine if the EmailService is available 129 */ 130 public static boolean isServiceAvailable(Context context, String protocol) { 131 EmailServiceInfo info = getServiceInfo(context, protocol); 132 if (info == null) return false; 133 if (info.klass != null) return true; 134 return new EmailServiceProxy(context, info.intentAction, null).test(); 135 } 136 137 /** 138 * For a given account id, return a service proxy if applicable, or null. 139 * 140 * @param accountId the message of interest 141 * @result service proxy, or null if n/a 142 */ 143 public static EmailServiceProxy getServiceForAccount(Context context, 144 IEmailServiceCallback callback, long accountId) { 145 return getService(context, callback, Account.getProtocol(context, accountId)); 146 } 147 148 /** 149 * Holder of service information (currently just name and class/intent); if there is a class 150 * member, this is a (local, i.e. same process) service; otherwise, this is a remote service 151 */ 152 public static class EmailServiceInfo { 153 public String protocol; 154 public String name; 155 public String accountType; 156 Class<? extends Service> klass; 157 String intentAction; 158 public int port; 159 public int portSsl; 160 public boolean defaultSsl; 161 public boolean offerTls; 162 public boolean offerCerts; 163 public boolean usesSmtp; 164 public boolean offerLocalDeletes; 165 public int defaultLocalDeletes; 166 public boolean offerPrefix; 167 public boolean usesAutodiscover; 168 public boolean offerLookback; 169 public int defaultLookback; 170 public boolean syncChanges; 171 public boolean syncContacts; 172 public boolean syncCalendar; 173 public boolean offerAttachmentPreload; 174 public CharSequence[] syncIntervalStrings; 175 public CharSequence[] syncIntervals; 176 public int defaultSyncInterval; 177 public String inferPrefix; 178 public boolean requiresAccountUpdate; 179 180 public String toString() { 181 StringBuilder sb = new StringBuilder("Protocol: "); 182 sb.append(protocol); 183 sb.append(", "); 184 sb.append(klass != null ? "Local" : "Remote"); 185 return sb.toString(); 186 } 187 } 188 189 public static EmailServiceProxy getService(Context context, IEmailServiceCallback callback, 190 String protocol) { 191 EmailServiceInfo info = null; 192 // Handle the degenerate case here (account might have been deleted) 193 if (protocol != null) { 194 info = getServiceInfo(context, protocol); 195 } 196 if (info == null) { 197 Log.w(Logging.LOG_TAG, "Returning NullService for " + protocol); 198 return new EmailServiceProxy(context, NullService.class, null); 199 } else { 200 return getServiceFromInfo(context, callback, info); 201 } 202 } 203 204 public static EmailServiceProxy getServiceFromInfo(Context context, 205 IEmailServiceCallback callback, EmailServiceInfo info) { 206 if (info.klass != null) { 207 return new EmailServiceProxy(context, info.klass, callback); 208 } else { 209 return new EmailServiceProxy(context, info.intentAction, callback); 210 } 211 } 212 213 public static EmailServiceInfo getServiceInfo(Context context, String protocol) { 214 if (sServiceList.isEmpty()) { 215 findServices(context); 216 } 217 for (EmailServiceInfo info: sServiceList) { 218 if (info.protocol.equals(protocol)) { 219 return info; 220 } 221 } 222 return null; 223 } 224 225 public static List<EmailServiceInfo> getServiceInfoList(Context context) { 226 synchronized(sServiceList) { 227 if (sServiceList.isEmpty()) { 228 findServices(context); 229 } 230 return sServiceList; 231 } 232 } 233 234 private static void finishAccountManagerBlocker(AccountManagerFuture<?> future) { 235 try { 236 // Note: All of the potential errors are simply logged 237 // here, as there is nothing to actually do about them. 238 future.getResult(); 239 } catch (OperationCanceledException e) { 240 Log.w(Logging.LOG_TAG, e.toString()); 241 } catch (AuthenticatorException e) { 242 Log.w(Logging.LOG_TAG, e.toString()); 243 } catch (IOException e) { 244 Log.w(Logging.LOG_TAG, e.toString()); 245 } 246 } 247 248 private static class UpdateAccountManagerTask extends AsyncTask<Void, Void, Void> { 249 private final Context mContext; 250 private final android.accounts.Account mAccount; 251 private final EmailServiceInfo mOldInfo; 252 private final EmailServiceInfo mNewInfo; 253 254 public UpdateAccountManagerTask(Context context, android.accounts.Account amAccount, 255 EmailServiceInfo oldInfo, EmailServiceInfo newInfo) { 256 super(); 257 mContext = context; 258 mAccount = amAccount; 259 mOldInfo = oldInfo; 260 mNewInfo = newInfo; 261 } 262 263 @Override 264 protected Void doInBackground(Void... params) { 265 updateAccountManagerType(mContext, mAccount, mOldInfo, mNewInfo); 266 return null; 267 } 268 } 269 270 private static class DisableComponentsTask extends AsyncTask<Void, Void, Void> { 271 private final Context mContext; 272 273 public DisableComponentsTask(Context context) { 274 super(); 275 mContext = context; 276 } 277 278 @Override 279 protected Void doInBackground(Void... params) { 280 disableComponent(mContext, LegacyEmailAuthenticatorService.class); 281 disableComponent(mContext, LegacyEasAuthenticatorService.class); 282 return null; 283 } 284 } 285 286 private static void updateAccountManagerType(Context context, 287 android.accounts.Account amAccount, EmailServiceInfo oldInfo, 288 EmailServiceInfo newInfo) { 289 ContentResolver resolver = context.getContentResolver(); 290 Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, 291 AccountColumns.EMAIL_ADDRESS + "=?", new String[] { amAccount.name }, null); 292 // That's odd, isn't it? 293 if (c == null) return; 294 try { 295 if (c.moveToNext()) { 296 // Get the EmailProvider Account/HostAuth 297 Account account = new Account(); 298 account.restore(c); 299 HostAuth hostAuth = 300 HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv); 301 if (hostAuth == null) return; 302 303 // Make sure this email address is using the expected protocol; our query to 304 // AccountManager doesn't know which protocol was being used (com.android.email 305 // was used for both pop3 and imap 306 if (!hostAuth.mProtocol.equals(oldInfo.protocol)) { 307 return; 308 } 309 Log.w(Logging.LOG_TAG, "Converting " + amAccount.name + " to " + newInfo.protocol); 310 311 ContentValues accountValues = new ContentValues(); 312 int oldFlags = account.mFlags; 313 314 // Mark the provider account incomplete so it can't get reconciled away 315 account.mFlags |= Account.FLAGS_INCOMPLETE; 316 accountValues.put(AccountColumns.FLAGS, account.mFlags); 317 Uri accountUri = ContentUris.withAppendedId(Account.CONTENT_URI, account.mId); 318 resolver.update(accountUri, accountValues, null, null); 319 320 // Change the HostAuth to reference the new protocol; this has to be done before 321 // trying to create the AccountManager account (below) 322 ContentValues hostValues = new ContentValues(); 323 hostValues.put(HostAuth.PROTOCOL, newInfo.protocol); 324 resolver.update(ContentUris.withAppendedId(HostAuth.CONTENT_URI, hostAuth.mId), 325 hostValues, null, null); 326 Log.w(Logging.LOG_TAG, "Updated HostAuths"); 327 328 try { 329 // Get current settings for the existing AccountManager account 330 boolean email = ContentResolver.getSyncAutomatically(amAccount, 331 EmailContent.AUTHORITY); 332 if (!email) { 333 // Try our old provider name 334 email = ContentResolver.getSyncAutomatically(amAccount, 335 "com.android.email.provider"); 336 } 337 boolean contacts = ContentResolver.getSyncAutomatically(amAccount, 338 ContactsContract.AUTHORITY); 339 boolean calendar = ContentResolver.getSyncAutomatically(amAccount, 340 CalendarContract.AUTHORITY); 341 Log.w(Logging.LOG_TAG, "Email: " + email + ", Contacts: " + contacts + "," + 342 " Calendar: " + calendar); 343 344 // Get sync keys for calendar/contacts 345 String amName = amAccount.name; 346 String oldType = amAccount.type; 347 ContentProviderClient client = context.getContentResolver() 348 .acquireContentProviderClient(CalendarContract.CONTENT_URI); 349 byte[] calendarSyncKey = null; 350 try { 351 calendarSyncKey = SyncStateContract.Helpers.get(client, 352 asCalendarSyncAdapter(SyncState.CONTENT_URI, amName, oldType), 353 new android.accounts.Account(amName, oldType)); 354 } catch (RemoteException e) { 355 Log.w(Logging.LOG_TAG, "Get calendar key FAILED"); 356 } finally { 357 client.release(); 358 } 359 client = context.getContentResolver() 360 .acquireContentProviderClient(ContactsContract.AUTHORITY_URI); 361 byte[] contactsSyncKey = null; 362 try { 363 contactsSyncKey = SyncStateContract.Helpers.get(client, 364 ContactsContract.SyncState.CONTENT_URI, 365 new android.accounts.Account(amName, oldType)); 366 } catch (RemoteException e) { 367 Log.w(Logging.LOG_TAG, "Get contacts key FAILED"); 368 } finally { 369 client.release(); 370 } 371 if (calendarSyncKey != null) { 372 Log.w(Logging.LOG_TAG, "Got calendar key: " + new String(calendarSyncKey)); 373 } 374 if (contactsSyncKey != null) { 375 Log.w(Logging.LOG_TAG, "Got contacts key: " + new String(contactsSyncKey)); 376 } 377 378 // Set up a new AccountManager account with new type and old settings 379 AccountManagerFuture<?> amFuture = MailService.setupAccountManagerAccount( 380 context, account, email, calendar, contacts, null); 381 finishAccountManagerBlocker(amFuture); 382 Log.w(Logging.LOG_TAG, "Created new AccountManager account"); 383 384 // Delete the AccountManager account 385 amFuture = AccountManager.get(context) 386 .removeAccount(amAccount, null, null); 387 finishAccountManagerBlocker(amFuture); 388 Log.w(Logging.LOG_TAG, "Deleted old AccountManager account"); 389 390 // Restore sync keys for contacts/calendar 391 if (calendarSyncKey != null && calendarSyncKey.length != 0) { 392 client = context.getContentResolver() 393 .acquireContentProviderClient(CalendarContract.CONTENT_URI); 394 try { 395 SyncStateContract.Helpers.set(client, 396 asCalendarSyncAdapter(SyncState.CONTENT_URI, amName, 397 newInfo.accountType), 398 new android.accounts.Account(amName, newInfo.accountType), 399 calendarSyncKey); 400 Log.w(Logging.LOG_TAG, "Set calendar key..."); 401 } catch (RemoteException e) { 402 Log.w(Logging.LOG_TAG, "Set calendar key FAILED"); 403 } finally { 404 client.release(); 405 } 406 } 407 if (contactsSyncKey != null && contactsSyncKey.length != 0) { 408 client = context.getContentResolver() 409 .acquireContentProviderClient(ContactsContract.AUTHORITY_URI); 410 try { 411 SyncStateContract.Helpers.set(client, 412 ContactsContract.SyncState.CONTENT_URI, 413 new android.accounts.Account(amName, newInfo.accountType), 414 contactsSyncKey); 415 Log.w(Logging.LOG_TAG, "Set contacts key..."); 416 } catch (RemoteException e) { 417 Log.w(Logging.LOG_TAG, "Set contacts key FAILED"); 418 } 419 } 420 421 if (oldInfo.requiresAccountUpdate) { 422 EmailServiceProxy service = 423 EmailServiceUtils.getServiceFromInfo(context, null, newInfo); 424 try { 425 service.serviceUpdated(amAccount.name); 426 Log.w(Logging.LOG_TAG, "Updated account settings"); 427 } catch (RemoteException e) { 428 // Old settings won't hurt anyone 429 } 430 } 431 432 // That's all folks! 433 Log.w(Logging.LOG_TAG, "Account update completed."); 434 } finally { 435 // Clear the incomplete flag on the provider account 436 accountValues.put(AccountColumns.FLAGS, oldFlags); 437 resolver.update(accountUri, accountValues, null, null); 438 Log.w(Logging.LOG_TAG, "[Incomplete flag cleared]"); 439 } 440 } 441 } finally { 442 c.close(); 443 } 444 } 445 446 private static void disableComponent(Context context, Class<?> klass) { 447 Log.w(Logging.LOG_TAG, "Disabling legacy authenticator " + klass.getSimpleName()); 448 final ComponentName c = new ComponentName(context, klass); 449 context.getPackageManager().setComponentEnabledSetting(c, 450 PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 451 PackageManager.DONT_KILL_APP); 452 } 453 454 /** 455 * Parse services.xml file to find our available email services 456 */ 457 @SuppressWarnings("unchecked") 458 private static synchronized void findServices(Context context) { 459 try { 460 Resources res = context.getResources(); 461 XmlResourceParser xml = res.getXml(R.xml.services); 462 int xmlEventType; 463 // walk through senders.xml file. 464 while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) { 465 if (xmlEventType == XmlResourceParser.START_TAG && 466 "emailservice".equals(xml.getName())) { 467 EmailServiceInfo info = new EmailServiceInfo(); 468 TypedArray ta = res.obtainAttributes(xml, R.styleable.EmailServiceInfo); 469 info.protocol = ta.getString(R.styleable.EmailServiceInfo_protocol); 470 info.accountType = ta.getString(R.styleable.EmailServiceInfo_accountType); 471 // Handle upgrade of one protocol to another (e.g. imap to imap2) 472 String newProtocol = ta.getString(R.styleable.EmailServiceInfo_replaceWith); 473 if (newProtocol != null) { 474 EmailServiceInfo newInfo = getServiceInfo(context, newProtocol); 475 if (newInfo == null) { 476 throw new IllegalStateException( 477 "Replacement service not found: " + newProtocol); 478 } 479 info.requiresAccountUpdate = ta.getBoolean( 480 R.styleable.EmailServiceInfo_requiresAccountUpdate, false); 481 AccountManager am = AccountManager.get(context); 482 android.accounts.Account[] amAccounts = 483 am.getAccountsByType(info.accountType); 484 for (android.accounts.Account amAccount: amAccounts) { 485 new UpdateAccountManagerTask(context, amAccount, info, newInfo) 486 .executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); 487 } 488 continue; 489 } 490 info.name = ta.getString(R.styleable.EmailServiceInfo_name); 491 String klass = ta.getString(R.styleable.EmailServiceInfo_serviceClass); 492 info.intentAction = ta.getString(R.styleable.EmailServiceInfo_intent); 493 info.defaultSsl = ta.getBoolean(R.styleable.EmailServiceInfo_defaultSsl, false); 494 info.port = ta.getInteger(R.styleable.EmailServiceInfo_port, 0); 495 info.portSsl = ta.getInteger(R.styleable.EmailServiceInfo_portSsl, 0); 496 info.offerTls = ta.getBoolean(R.styleable.EmailServiceInfo_offerTls, false); 497 info.offerCerts = ta.getBoolean(R.styleable.EmailServiceInfo_offerCerts, false); 498 info.offerLocalDeletes = 499 ta.getBoolean(R.styleable.EmailServiceInfo_offerLocalDeletes, false); 500 info.defaultLocalDeletes = 501 ta.getInteger(R.styleable.EmailServiceInfo_defaultLocalDeletes, 502 Account.DELETE_POLICY_ON_DELETE); 503 info.offerPrefix = 504 ta.getBoolean(R.styleable.EmailServiceInfo_offerPrefix, false); 505 info.usesSmtp = ta.getBoolean(R.styleable.EmailServiceInfo_usesSmtp, false); 506 info.usesAutodiscover = 507 ta.getBoolean(R.styleable.EmailServiceInfo_usesAutodiscover, false); 508 info.offerLookback = 509 ta.getBoolean(R.styleable.EmailServiceInfo_offerLookback, false); 510 info.defaultLookback = 511 ta.getInteger(R.styleable.EmailServiceInfo_defaultLookback, 512 SyncWindow.SYNC_WINDOW_3_DAYS); 513 info.syncChanges = 514 ta.getBoolean(R.styleable.EmailServiceInfo_syncChanges, false); 515 info.syncContacts = 516 ta.getBoolean(R.styleable.EmailServiceInfo_syncContacts, false); 517 info.syncCalendar = 518 ta.getBoolean(R.styleable.EmailServiceInfo_syncCalendar, false); 519 info.offerAttachmentPreload = 520 ta.getBoolean(R.styleable.EmailServiceInfo_offerAttachmentPreload, false); 521 info.syncIntervalStrings = 522 ta.getTextArray(R.styleable.EmailServiceInfo_syncIntervalStrings); 523 info.syncIntervals = 524 ta.getTextArray(R.styleable.EmailServiceInfo_syncIntervals); 525 info.defaultSyncInterval = 526 ta.getInteger(R.styleable.EmailServiceInfo_defaultSyncInterval, 15); 527 info.inferPrefix = ta.getString(R.styleable.EmailServiceInfo_inferPrefix); 528 529 // Must have either "class" (local) or "intent" (remote) 530 if (klass != null) { 531 try { 532 info.klass = (Class<? extends Service>) Class.forName(klass); 533 } catch (ClassNotFoundException e) { 534 throw new IllegalStateException( 535 "Class not found in service descriptor: " + klass); 536 } 537 } 538 if (info.klass == null && info.intentAction == null) { 539 throw new IllegalStateException( 540 "No class or intent action specified in service descriptor"); 541 } 542 if (info.klass != null && info.intentAction != null) { 543 throw new IllegalStateException( 544 "Both class and intent action specified in service descriptor"); 545 } 546 sServiceList.add(info); 547 } 548 } 549 // Disable our legacy components 550 new DisableComponentsTask(context).executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); 551 } catch (XmlPullParserException e) { 552 // ignore 553 } catch (IOException e) { 554 // ignore 555 } 556 } 557 558 private static Uri asCalendarSyncAdapter(Uri uri, String account, String accountType) { 559 return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") 560 .appendQueryParameter(Calendars.ACCOUNT_NAME, account) 561 .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build(); 562 } 563 564 /** 565 * A no-op service that can be returned for non-existent/null protocols 566 */ 567 class NullService implements IEmailService { 568 @Override 569 public IBinder asBinder() { 570 return null; 571 } 572 573 @Override 574 public Bundle validate(HostAuth hostauth) throws RemoteException { 575 return null; 576 } 577 578 @Override 579 public void startSync(long mailboxId, boolean userRequest) throws RemoteException { 580 } 581 582 @Override 583 public void stopSync(long mailboxId) throws RemoteException { 584 } 585 586 @Override 587 public void loadMore(long messageId) throws RemoteException { 588 } 589 590 @Override 591 public void loadAttachment(long attachmentId, boolean background) throws RemoteException { 592 } 593 594 @Override 595 public void updateFolderList(long accountId) throws RemoteException { 596 } 597 598 @Override 599 public boolean createFolder(long accountId, String name) throws RemoteException { 600 return false; 601 } 602 603 @Override 604 public boolean deleteFolder(long accountId, String name) throws RemoteException { 605 return false; 606 } 607 608 @Override 609 public boolean renameFolder(long accountId, String oldName, String newName) 610 throws RemoteException { 611 return false; 612 } 613 614 @Override 615 public void setCallback(IEmailServiceCallback cb) throws RemoteException { 616 } 617 618 @Override 619 public void setLogging(int on) throws RemoteException { 620 } 621 622 @Override 623 public void hostChanged(long accountId) throws RemoteException { 624 } 625 626 @Override 627 public Bundle autoDiscover(String userName, String password) throws RemoteException { 628 return null; 629 } 630 631 @Override 632 public void sendMeetingResponse(long messageId, int response) throws RemoteException { 633 } 634 635 @Override 636 public void deleteAccountPIMData(long accountId) throws RemoteException { 637 } 638 639 @Override 640 public int getApiLevel() throws RemoteException { 641 return Api.LEVEL; 642 } 643 644 @Override 645 public int searchMessages(long accountId, SearchParams params, long destMailboxId) 646 throws RemoteException { 647 return 0; 648 } 649 650 @Override 651 public void sendMail(long accountId) throws RemoteException { 652 } 653 654 @Override 655 public void serviceUpdated(String emailAddress) throws RemoteException { 656 } 657 658 @Override 659 public int getCapabilities(Account acct) throws RemoteException { 660 return 0; 661 } 662 } 663} 664