• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 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 
17 package com.android.email;
18 
19 import android.app.Service;
20 import android.content.ContentResolver;
21 import android.content.ContentUris;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.os.Bundle;
28 import android.os.IBinder;
29 import android.os.RemoteCallbackList;
30 import android.os.RemoteException;
31 import android.util.Log;
32 
33 import com.android.email.mail.store.Pop3Store.Pop3Message;
34 import com.android.email.provider.AccountBackupRestore;
35 import com.android.email.service.EmailServiceUtils;
36 import com.android.email.service.MailService;
37 import com.android.emailcommon.Api;
38 import com.android.emailcommon.Logging;
39 import com.android.emailcommon.mail.AuthenticationFailedException;
40 import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
41 import com.android.emailcommon.mail.MessagingException;
42 import com.android.emailcommon.provider.Account;
43 import com.android.emailcommon.provider.EmailContent;
44 import com.android.emailcommon.provider.EmailContent.Attachment;
45 import com.android.emailcommon.provider.EmailContent.Body;
46 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
47 import com.android.emailcommon.provider.EmailContent.Message;
48 import com.android.emailcommon.provider.EmailContent.MessageColumns;
49 import com.android.emailcommon.provider.HostAuth;
50 import com.android.emailcommon.provider.Mailbox;
51 import com.android.emailcommon.service.EmailServiceStatus;
52 import com.android.emailcommon.service.IEmailService;
53 import com.android.emailcommon.service.IEmailServiceCallback;
54 import com.android.emailcommon.service.SearchParams;
55 import com.android.emailcommon.utility.AttachmentUtilities;
56 import com.android.emailcommon.utility.EmailAsyncTask;
57 import com.android.emailcommon.utility.Utility;
58 import com.google.common.annotations.VisibleForTesting;
59 
60 import java.io.FileNotFoundException;
61 import java.io.IOException;
62 import java.io.InputStream;
63 import java.util.ArrayList;
64 import java.util.Collection;
65 import java.util.HashMap;
66 import java.util.HashSet;
67 import java.util.concurrent.ConcurrentHashMap;
68 
69 /**
70  * New central controller/dispatcher for Email activities that may require remote operations.
71  * Handles disambiguating between legacy MessagingController operations and newer provider/sync
72  * based code.  We implement Service to allow loadAttachment calls to be sent in a consistent manner
73  * to IMAP, POP3, and EAS by AttachmentDownloadService
74  */
75 public class Controller {
76     private static final String TAG = "Controller";
77     private static Controller sInstance;
78     private final Context mContext;
79     private Context mProviderContext;
80     private final MessagingController mLegacyController;
81     private final LegacyListener mLegacyListener = new LegacyListener();
82     private final ServiceCallback mServiceCallback = new ServiceCallback();
83     private final HashSet<Result> mListeners = new HashSet<Result>();
84     /*package*/ final ConcurrentHashMap<Long, Boolean> mLegacyControllerMap =
85         new ConcurrentHashMap<Long, Boolean>();
86 
87     // Note that 0 is a syntactically valid account key; however there can never be an account
88     // with id = 0, so attempts to restore the account will return null.  Null values are
89     // handled properly within the code, so this won't cause any issues.
90     private static final long GLOBAL_MAILBOX_ACCOUNT_KEY = 0;
91     /*package*/ static final String ATTACHMENT_MAILBOX_SERVER_ID = "__attachment_mailbox__";
92     /*package*/ static final String ATTACHMENT_MESSAGE_UID_PREFIX = "__attachment_message__";
93     /*package*/ static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__";
94     private static final String WHERE_TYPE_ATTACHMENT =
95         MailboxColumns.TYPE + "=" + Mailbox.TYPE_ATTACHMENT;
96     private static final String WHERE_MAILBOX_KEY = MessageColumns.MAILBOX_KEY + "=?";
97 
98     private static final String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] {
99         EmailContent.RECORD_ID,
100         EmailContent.MessageColumns.ACCOUNT_KEY
101     };
102     private static final int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1;
103 
104     private static final String[] BODY_SOURCE_KEY_PROJECTION =
105         new String[] {Body.SOURCE_MESSAGE_KEY};
106     private static final int BODY_SOURCE_KEY_COLUMN = 0;
107     private static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?";
108 
109     private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?";
110     private static final String MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION =
111         MAILBOXES_FOR_ACCOUNT_SELECTION + " AND " + MailboxColumns.TYPE + "!=" +
112         Mailbox.TYPE_EAS_ACCOUNT_MAILBOX;
113     private static final String MESSAGES_FOR_ACCOUNT_SELECTION = MessageColumns.ACCOUNT_KEY + "=?";
114 
115     // Service callbacks as set up via setCallback
116     private static RemoteCallbackList<IEmailServiceCallback> sCallbackList =
117         new RemoteCallbackList<IEmailServiceCallback>();
118 
119     private volatile boolean mInUnitTests = false;
120 
Controller(Context _context)121     protected Controller(Context _context) {
122         mContext = _context.getApplicationContext();
123         mProviderContext = _context;
124         mLegacyController = MessagingController.getInstance(mProviderContext, this);
125         mLegacyController.addListener(mLegacyListener);
126     }
127 
128     /**
129      * Mark this controller as being in use in a unit test.
130      * This is a kludge vs having proper mocks and dependency injection; since the Controller is a
131      * global singleton there isn't much else we can do.
132      */
markForTest(boolean inUnitTests)133     public void markForTest(boolean inUnitTests) {
134         mInUnitTests = inUnitTests;
135     }
136 
137     /**
138      * Cleanup for test.  Mustn't be called for the regular {@link Controller}, as it's a
139      * singleton and lives till the process finishes.
140      *
141      * <p>However, this method MUST be called for mock instances.
142      */
cleanupForTest()143     public void cleanupForTest() {
144         mLegacyController.removeListener(mLegacyListener);
145     }
146 
147     /**
148      * Gets or creates the singleton instance of Controller.
149      */
getInstance(Context _context)150     public synchronized static Controller getInstance(Context _context) {
151         if (sInstance == null) {
152             sInstance = new Controller(_context);
153         }
154         return sInstance;
155     }
156 
157     /**
158      * Inject a mock controller.  Used only for testing.  Affects future calls to getInstance().
159      *
160      * Tests that use this method MUST clean it up by calling this method again with null.
161      */
injectMockControllerForTest(Controller mockController)162     public synchronized static void injectMockControllerForTest(Controller mockController) {
163         sInstance = mockController;
164     }
165 
166     /**
167      * For testing only:  Inject a different context for provider access.  This will be
168      * used internally for access the underlying provider (e.g. getContentResolver().query()).
169      * @param providerContext the provider context to be used by this instance
170      */
setProviderContext(Context providerContext)171     public void setProviderContext(Context providerContext) {
172         mProviderContext = providerContext;
173     }
174 
175     /**
176      * Any UI code that wishes for callback results (on async ops) should register their callback
177      * here (typically from onResume()).  Unregistered callbacks will never be called, to prevent
178      * problems when the command completes and the activity has already paused or finished.
179      * @param listener The callback that may be used in action methods
180      */
addResultCallback(Result listener)181     public void addResultCallback(Result listener) {
182         synchronized (mListeners) {
183             listener.setRegistered(true);
184             mListeners.add(listener);
185         }
186     }
187 
188     /**
189      * Any UI code that no longer wishes for callback results (on async ops) should unregister
190      * their callback here (typically from onPause()).  Unregistered callbacks will never be called,
191      * to prevent problems when the command completes and the activity has already paused or
192      * finished.
193      * @param listener The callback that may no longer be used
194      */
removeResultCallback(Result listener)195     public void removeResultCallback(Result listener) {
196         synchronized (mListeners) {
197             listener.setRegistered(false);
198             mListeners.remove(listener);
199         }
200     }
201 
getResultCallbacksForTest()202     public Collection<Result> getResultCallbacksForTest() {
203         return mListeners;
204     }
205 
206     /**
207      * Delete all Messages that live in the attachment mailbox
208      */
deleteAttachmentMessages()209     public void deleteAttachmentMessages() {
210         // Note: There should only be one attachment mailbox at present
211         ContentResolver resolver = mProviderContext.getContentResolver();
212         Cursor c = null;
213         try {
214             c = resolver.query(Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION,
215                     WHERE_TYPE_ATTACHMENT, null, null);
216             while (c.moveToNext()) {
217                 long mailboxId = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
218                 // Must delete attachments BEFORE messages
219                 AttachmentUtilities.deleteAllMailboxAttachmentFiles(mProviderContext, 0,
220                         mailboxId);
221                 resolver.delete(Message.CONTENT_URI, WHERE_MAILBOX_KEY,
222                         new String[] {Long.toString(mailboxId)});
223            }
224         } finally {
225             if (c != null) {
226                 c.close();
227             }
228         }
229     }
230 
231     /**
232      * Get a mailbox based on a sqlite WHERE clause
233      */
getGlobalMailboxWhere(String where)234     private Mailbox getGlobalMailboxWhere(String where) {
235         Cursor c = mProviderContext.getContentResolver().query(Mailbox.CONTENT_URI,
236                 Mailbox.CONTENT_PROJECTION, where, null, null);
237         try {
238             if (c.moveToFirst()) {
239                 Mailbox m = new Mailbox();
240                 m.restore(c);
241                 return m;
242             }
243         } finally {
244             c.close();
245         }
246         return null;
247     }
248 
249     /**
250      * Returns the attachment mailbox (where we store eml attachment Emails), creating one
251      * if necessary
252      * @return the global attachment mailbox
253      */
getAttachmentMailbox()254     public Mailbox getAttachmentMailbox() {
255         Mailbox m = getGlobalMailboxWhere(WHERE_TYPE_ATTACHMENT);
256         if (m == null) {
257             m = new Mailbox();
258             m.mAccountKey = GLOBAL_MAILBOX_ACCOUNT_KEY;
259             m.mServerId = ATTACHMENT_MAILBOX_SERVER_ID;
260             m.mFlagVisible = false;
261             m.mDisplayName = ATTACHMENT_MAILBOX_SERVER_ID;
262             m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER;
263             m.mType = Mailbox.TYPE_ATTACHMENT;
264             m.save(mProviderContext);
265         }
266         return m;
267     }
268 
269     /**
270      * Returns the search mailbox for the specified account, creating one if necessary
271      * @return the search mailbox for the passed in account
272      */
getSearchMailbox(long accountId)273     public Mailbox getSearchMailbox(long accountId) {
274         Mailbox m = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_SEARCH);
275         if (m == null) {
276             m = new Mailbox();
277             m.mAccountKey = accountId;
278             m.mServerId = SEARCH_MAILBOX_SERVER_ID;
279             m.mFlagVisible = false;
280             m.mDisplayName = SEARCH_MAILBOX_SERVER_ID;
281             m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER;
282             m.mType = Mailbox.TYPE_SEARCH;
283             m.mFlags = Mailbox.FLAG_HOLDS_MAIL;
284             m.mParentKey = Mailbox.NO_MAILBOX;
285             m.save(mProviderContext);
286         }
287         return m;
288     }
289 
290     /**
291      * Create a Message from the Uri and store it in the attachment mailbox
292      * @param uri the uri containing message content
293      * @return the Message or null
294      */
loadMessageFromUri(Uri uri)295     public Message loadMessageFromUri(Uri uri) {
296         Mailbox mailbox = getAttachmentMailbox();
297         if (mailbox == null) return null;
298         try {
299             InputStream is = mProviderContext.getContentResolver().openInputStream(uri);
300             try {
301                 // First, create a Pop3Message from the attachment and then parse it
302                 Pop3Message pop3Message = new Pop3Message(
303                         ATTACHMENT_MESSAGE_UID_PREFIX + System.currentTimeMillis(), null);
304                 pop3Message.parse(is);
305                 // Now, pull out the header fields
306                 Message msg = new Message();
307                 LegacyConversions.updateMessageFields(msg, pop3Message, 0, mailbox.mId);
308                 // Commit the message to the local store
309                 msg.save(mProviderContext);
310                 // Setup the rest of the message and mark it completely loaded
311                 mLegacyController.copyOneMessageToProvider(pop3Message, msg,
312                         Message.FLAG_LOADED_COMPLETE, mProviderContext);
313                 // Restore the complete message and return it
314                 return Message.restoreMessageWithId(mProviderContext, msg.mId);
315             } catch (MessagingException e) {
316             } catch (IOException e) {
317             }
318         } catch (FileNotFoundException e) {
319         }
320         return null;
321     }
322 
323     /**
324      * Set logging flags for external sync services
325      *
326      * Generally this should be called by anybody who changes Email.DEBUG
327      */
serviceLogging(int debugFlags)328     public void serviceLogging(int debugFlags) {
329         IEmailService service = EmailServiceUtils.getExchangeService(mContext, mServiceCallback);
330         try {
331             service.setLogging(debugFlags);
332         } catch (RemoteException e) {
333             // TODO Change exception handling to be consistent with however this method
334             // is implemented for other protocols
335             Log.d("setLogging", "RemoteException" + e);
336         }
337     }
338 
339     /**
340      * Request a remote update of mailboxes for an account.
341      */
updateMailboxList(final long accountId)342     public void updateMailboxList(final long accountId) {
343         Utility.runAsync(new Runnable() {
344             @Override
345             public void run() {
346                 final IEmailService service = getServiceForAccount(accountId);
347                 if (service != null) {
348                     // Service implementation
349                     try {
350                         service.updateFolderList(accountId);
351                     } catch (RemoteException e) {
352                         // TODO Change exception handling to be consistent with however this method
353                         // is implemented for other protocols
354                         Log.d("updateMailboxList", "RemoteException" + e);
355                     }
356                 } else {
357                     // MessagingController implementation
358                     mLegacyController.listFolders(accountId, mLegacyListener);
359                 }
360             }
361         });
362     }
363 
364     /**
365      * Request a remote update of a mailbox.  For use by the timed service.
366      *
367      * Functionally this is quite similar to updateMailbox(), but it's a separate API and
368      * separate callback in order to keep UI callbacks from affecting the service loop.
369      */
serviceCheckMail(final long accountId, final long mailboxId, final long tag)370     public void serviceCheckMail(final long accountId, final long mailboxId, final long tag) {
371         IEmailService service = getServiceForAccount(accountId);
372         if (service != null) {
373             // Service implementation
374 //            try {
375                 // TODO this isn't quite going to work, because we're going to get the
376                 // generic (UI) callbacks and not the ones we need to restart the ol' service.
377                 // service.startSync(mailboxId, tag);
378             mLegacyListener.checkMailFinished(mContext, accountId, mailboxId, tag);
379 //            } catch (RemoteException e) {
380                 // TODO Change exception handling to be consistent with however this method
381                 // is implemented for other protocols
382 //                Log.d("updateMailbox", "RemoteException" + e);
383 //            }
384         } else {
385             // MessagingController implementation
386             Utility.runAsync(new Runnable() {
387                 public void run() {
388                     mLegacyController.checkMail(accountId, tag, mLegacyListener);
389                 }
390             });
391         }
392     }
393 
394     /**
395      * Request a remote update of a mailbox.
396      *
397      * The contract here should be to try and update the headers ASAP, in order to populate
398      * a simple message list.  We should also at this point queue up a background task of
399      * downloading some/all of the messages in this mailbox, but that should be interruptable.
400      */
updateMailbox(final long accountId, final long mailboxId, boolean userRequest)401     public void updateMailbox(final long accountId, final long mailboxId, boolean userRequest) {
402 
403         IEmailService service = getServiceForAccount(accountId);
404         if (service != null) {
405            try {
406                 service.startSync(mailboxId, userRequest);
407             } catch (RemoteException e) {
408                 // TODO Change exception handling to be consistent with however this method
409                 // is implemented for other protocols
410                 Log.d("updateMailbox", "RemoteException" + e);
411             }
412         } else {
413             // MessagingController implementation
414             Utility.runAsync(new Runnable() {
415                 public void run() {
416                     // TODO shouldn't be passing fully-build accounts & mailboxes into APIs
417                     Account account =
418                         Account.restoreAccountWithId(mProviderContext, accountId);
419                     Mailbox mailbox =
420                         Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
421                     if (account == null || mailbox == null ||
422                             mailbox.mType == Mailbox.TYPE_SEARCH) {
423                         return;
424                     }
425                     mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
426                 }
427             });
428         }
429     }
430 
431     /**
432      * Request that any final work necessary be done, to load a message.
433      *
434      * Note, this assumes that the caller has already checked message.mFlagLoaded and that
435      * additional work is needed.  There is no optimization here for a message which is already
436      * loaded.
437      *
438      * @param messageId the message to load
439      * @param callback the Controller callback by which results will be reported
440      */
loadMessageForView(final long messageId)441     public void loadMessageForView(final long messageId) {
442 
443         // Split here for target type (Service or MessagingController)
444         IEmailService service = getServiceForMessage(messageId);
445         if (service != null) {
446             // There is no service implementation, so we'll just jam the value, log the error,
447             // and get out of here.
448             Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId);
449             ContentValues cv = new ContentValues();
450             cv.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE);
451             mProviderContext.getContentResolver().update(uri, cv, null, null);
452             Log.d(Logging.LOG_TAG, "Unexpected loadMessageForView() for service-based message.");
453             final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
454             synchronized (mListeners) {
455                 for (Result listener : mListeners) {
456                     listener.loadMessageForViewCallback(null, accountId, messageId, 100);
457                 }
458             }
459         } else {
460             // MessagingController implementation
461             Utility.runAsync(new Runnable() {
462                 public void run() {
463                     mLegacyController.loadMessageForView(messageId, mLegacyListener);
464                 }
465             });
466         }
467     }
468 
469 
470     /**
471      * Saves the message to a mailbox of given type.
472      * This is a synchronous operation taking place in the same thread as the caller.
473      * Upon return the message.mId is set.
474      * @param message the message (must have the mAccountId set).
475      * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS).
476      */
saveToMailbox(final EmailContent.Message message, final int mailboxType)477     public void saveToMailbox(final EmailContent.Message message, final int mailboxType) {
478         long accountId = message.mAccountKey;
479         long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType);
480         message.mMailboxKey = mailboxId;
481         message.save(mProviderContext);
482     }
483 
484     /**
485      * Look for a specific system mailbox, creating it if necessary, and return the mailbox id.
486      * This is a blocking operation and should not be called from the UI thread.
487      *
488      * Synchronized so multiple threads can call it (and not risk creating duplicate boxes).
489      *
490      * @param accountId the account id
491      * @param mailboxType the mailbox type (e.g.  EmailContent.Mailbox.TYPE_TRASH)
492      * @return the id of the mailbox. The mailbox is created if not existing.
493      * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative.
494      * Does not validate the input in other ways (e.g. does not verify the existence of account).
495      */
findOrCreateMailboxOfType(long accountId, int mailboxType)496     public synchronized long findOrCreateMailboxOfType(long accountId, int mailboxType) {
497         if (accountId < 0 || mailboxType < 0) {
498             return Mailbox.NO_MAILBOX;
499         }
500         long mailboxId =
501             Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType);
502         return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId;
503     }
504 
505     /**
506      * Returns the server-side name for a specific mailbox.
507      *
508      * @return the resource string corresponding to the mailbox type, empty if not found.
509      */
getMailboxServerName(Context context, int mailboxType)510     public static String getMailboxServerName(Context context, int mailboxType) {
511         int resId = -1;
512         switch (mailboxType) {
513             case Mailbox.TYPE_INBOX:
514                 resId = R.string.mailbox_name_server_inbox;
515                 break;
516             case Mailbox.TYPE_OUTBOX:
517                 resId = R.string.mailbox_name_server_outbox;
518                 break;
519             case Mailbox.TYPE_DRAFTS:
520                 resId = R.string.mailbox_name_server_drafts;
521                 break;
522             case Mailbox.TYPE_TRASH:
523                 resId = R.string.mailbox_name_server_trash;
524                 break;
525             case Mailbox.TYPE_SENT:
526                 resId = R.string.mailbox_name_server_sent;
527                 break;
528             case Mailbox.TYPE_JUNK:
529                 resId = R.string.mailbox_name_server_junk;
530                 break;
531         }
532         return resId != -1 ? context.getString(resId) : "";
533     }
534 
535     /**
536      * Create a mailbox given the account and mailboxType.
537      * TODO: Does this need to be signaled explicitly to the sync engines?
538      */
539     @VisibleForTesting
createMailbox(long accountId, int mailboxType)540     long createMailbox(long accountId, int mailboxType) {
541         if (accountId < 0 || mailboxType < 0) {
542             String mes = "Invalid arguments " + accountId + ' ' + mailboxType;
543             Log.e(Logging.LOG_TAG, mes);
544             throw new RuntimeException(mes);
545         }
546         Mailbox box = Mailbox.newSystemMailbox(
547                 accountId, mailboxType, getMailboxServerName(mContext, mailboxType));
548         box.save(mProviderContext);
549         return box.mId;
550     }
551 
552     /**
553      * Send a message:
554      * - move the message to Outbox (the message is assumed to be in Drafts).
555      * - EAS service will take it from there
556      * - mark reply/forward state in source message (if any)
557      * - trigger send for POP/IMAP
558      * @param message the fully populated Message (usually retrieved from the Draft box). Note that
559      *     all transient fields (e.g. Body related fields) are also expected to be fully loaded
560      */
sendMessage(Message message)561     public void sendMessage(Message message) {
562         ContentResolver resolver = mProviderContext.getContentResolver();
563         long accountId = message.mAccountKey;
564         long messageId = message.mId;
565         if (accountId == Account.NO_ACCOUNT) {
566             accountId = lookupAccountForMessage(messageId);
567         }
568         if (accountId == Account.NO_ACCOUNT) {
569             // probably the message was not found
570             if (Logging.LOGD) {
571                 Email.log("no account found for message " + messageId);
572             }
573             return;
574         }
575 
576         // Move to Outbox
577         long outboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_OUTBOX);
578         ContentValues cv = new ContentValues();
579         cv.put(EmailContent.MessageColumns.MAILBOX_KEY, outboxId);
580 
581         // does this need to be SYNCED_CONTENT_URI instead?
582         Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId);
583         resolver.update(uri, cv, null, null);
584 
585         // If this is a reply/forward, indicate it as such on the source.
586         long sourceKey = message.mSourceKey;
587         if (sourceKey != Message.NO_MESSAGE) {
588             boolean isReply = (message.mFlags & Message.FLAG_TYPE_REPLY) != 0;
589             int flagUpdate = isReply ? Message.FLAG_REPLIED_TO : Message.FLAG_FORWARDED;
590             setMessageAnsweredOrForwarded(sourceKey, flagUpdate);
591         }
592 
593         sendPendingMessages(accountId);
594     }
595 
sendPendingMessagesSmtp(long accountId)596     private void sendPendingMessagesSmtp(long accountId) {
597         // for IMAP & POP only, (attempt to) send the message now
598         final Account account =
599                 Account.restoreAccountWithId(mProviderContext, accountId);
600         if (account == null) {
601             return;
602         }
603         final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT);
604         Utility.runAsync(new Runnable() {
605             public void run() {
606                 mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener);
607             }
608         });
609     }
610 
611     /**
612      * Try to send all pending messages for a given account
613      *
614      * @param accountId the account for which to send messages
615      */
sendPendingMessages(long accountId)616     public void sendPendingMessages(long accountId) {
617         // 1. make sure we even have an outbox, exit early if not
618         final long outboxId =
619             Mailbox.findMailboxOfType(mProviderContext, accountId, Mailbox.TYPE_OUTBOX);
620         if (outboxId == Mailbox.NO_MAILBOX) {
621             return;
622         }
623 
624         // 2. dispatch as necessary
625         IEmailService service = getServiceForAccount(accountId);
626         if (service != null) {
627             // Service implementation
628             try {
629                 service.startSync(outboxId, false);
630             } catch (RemoteException e) {
631                 // TODO Change exception handling to be consistent with however this method
632                 // is implemented for other protocols
633                 Log.d("updateMailbox", "RemoteException" + e);
634             }
635         } else {
636             // MessagingController implementation
637             sendPendingMessagesSmtp(accountId);
638         }
639     }
640 
641     /**
642      * Reset visible limits for all accounts.
643      * For each account:
644      *   look up limit
645      *   write limit into all mailboxes for that account
646      */
resetVisibleLimits()647     public void resetVisibleLimits() {
648         Utility.runAsync(new Runnable() {
649             public void run() {
650                 ContentResolver resolver = mProviderContext.getContentResolver();
651                 Cursor c = null;
652                 try {
653                     c = resolver.query(
654                             Account.CONTENT_URI,
655                             Account.ID_PROJECTION,
656                             null, null, null);
657                     while (c.moveToNext()) {
658                         long accountId = c.getLong(Account.ID_PROJECTION_COLUMN);
659                         String protocol = Account.getProtocol(mProviderContext, accountId);
660                         if (!HostAuth.SCHEME_EAS.equals(protocol)) {
661                             ContentValues cv = new ContentValues();
662                             cv.put(MailboxColumns.VISIBLE_LIMIT, Email.VISIBLE_LIMIT_DEFAULT);
663                             resolver.update(Mailbox.CONTENT_URI, cv,
664                                     MailboxColumns.ACCOUNT_KEY + "=?",
665                                     new String[] { Long.toString(accountId) });
666                         }
667                     }
668                 } finally {
669                     if (c != null) {
670                         c.close();
671                     }
672                 }
673             }
674         });
675     }
676 
677     /**
678      * Increase the load count for a given mailbox, and trigger a refresh.  Applies only to
679      * IMAP and POP mailboxes, with the exception of the EAS search mailbox.
680      *
681      * @param mailboxId the mailbox
682      */
loadMoreMessages(final long mailboxId)683     public void loadMoreMessages(final long mailboxId) {
684         EmailAsyncTask.runAsyncParallel(new Runnable() {
685             public void run() {
686                 Mailbox mailbox = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
687                 if (mailbox == null) {
688                     return;
689                 }
690                 if (mailbox.mType == Mailbox.TYPE_SEARCH) {
691                     try {
692                         searchMore(mailbox.mAccountKey);
693                     } catch (MessagingException e) {
694                         // Nothing to be done
695                     }
696                     return;
697                 }
698                 Account account = Account.restoreAccountWithId(mProviderContext,
699                         mailbox.mAccountKey);
700                 if (account == null) {
701                     return;
702                 }
703                 // Use provider math to increment the field
704                 ContentValues cv = new ContentValues();;
705                 cv.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT);
706                 cv.put(EmailContent.ADD_COLUMN_NAME, Email.VISIBLE_LIMIT_INCREMENT);
707                 Uri uri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, mailboxId);
708                 mProviderContext.getContentResolver().update(uri, cv, null, null);
709                 // Trigger a refresh using the new, longer limit
710                 mailbox.mVisibleLimit += Email.VISIBLE_LIMIT_INCREMENT;
711                 mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
712             }
713         });
714     }
715 
716     /**
717      * @param messageId the id of message
718      * @return the accountId corresponding to the given messageId, or -1 if not found.
719      */
lookupAccountForMessage(long messageId)720     private long lookupAccountForMessage(long messageId) {
721         ContentResolver resolver = mProviderContext.getContentResolver();
722         Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
723                                   MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?",
724                                   new String[] { Long.toString(messageId) }, null);
725         try {
726             return c.moveToFirst()
727                 ? c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID)
728                 : -1;
729         } finally {
730             c.close();
731         }
732     }
733 
734     /**
735      * Delete a single attachment entry from the DB given its id.
736      * Does not delete any eventual associated files.
737      */
deleteAttachment(long attachmentId)738     public void deleteAttachment(long attachmentId) {
739         ContentResolver resolver = mProviderContext.getContentResolver();
740         Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId);
741         resolver.delete(uri, null, null);
742     }
743 
744     /**
745      * Async version of {@link #deleteMessageSync}.
746      */
deleteMessage(final long messageId)747     public void deleteMessage(final long messageId) {
748         EmailAsyncTask.runAsyncParallel(new Runnable() {
749             public void run() {
750                 deleteMessageSync(messageId);
751             }
752         });
753     }
754 
755     /**
756      * Batch & async version of {@link #deleteMessageSync}.
757      */
deleteMessages(final long[] messageIds)758     public void deleteMessages(final long[] messageIds) {
759         if (messageIds == null || messageIds.length == 0) {
760             throw new IllegalArgumentException();
761         }
762         EmailAsyncTask.runAsyncParallel(new Runnable() {
763             public void run() {
764                 for (long messageId: messageIds) {
765                     deleteMessageSync(messageId);
766                 }
767             }
768         });
769     }
770 
771     /**
772      * Delete a single message by moving it to the trash, or really delete it if it's already in
773      * trash or a draft message.
774      *
775      * This function has no callback, no result reporting, because the desired outcome
776      * is reflected entirely by changes to one or more cursors.
777      *
778      * @param messageId The id of the message to "delete".
779      */
deleteMessageSync(long messageId)780     /* package */ void deleteMessageSync(long messageId) {
781         // 1. Get the message's account
782         Account account = Account.getAccountForMessageId(mProviderContext, messageId);
783 
784         if (account == null) return;
785 
786         // 2. Confirm that there is a trash mailbox available.  If not, create one
787         long trashMailboxId = findOrCreateMailboxOfType(account.mId, Mailbox.TYPE_TRASH);
788 
789         // 3. Get the message's original mailbox
790         Mailbox mailbox = Mailbox.getMailboxForMessageId(mProviderContext, messageId);
791 
792         if (mailbox == null) return;
793 
794         // 4.  Drop non-essential data for the message (e.g. attachment files)
795         AttachmentUtilities.deleteAllAttachmentFiles(mProviderContext, account.mId,
796                 messageId);
797 
798         Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI,
799                 messageId);
800         ContentResolver resolver = mProviderContext.getContentResolver();
801 
802         // 5. Perform "delete" as appropriate
803         if ((mailbox.mId == trashMailboxId) || (mailbox.mType == Mailbox.TYPE_DRAFTS)) {
804             // 5a. Really delete it
805             resolver.delete(uri, null, null);
806         } else {
807             // 5b. Move to trash
808             ContentValues cv = new ContentValues();
809             cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId);
810             resolver.update(uri, cv, null, null);
811         }
812 
813         if (isMessagingController(account)) {
814             mLegacyController.processPendingActions(account.mId);
815         }
816     }
817 
818     /**
819      * Moves messages to a new mailbox.
820      *
821      * This function has no callback, no result reporting, because the desired outcome
822      * is reflected entirely by changes to one or more cursors.
823      *
824      * Note this method assumes all of the given message and mailbox IDs belong to the same
825      * account.
826      *
827      * @param messageIds IDs of the messages that are to be moved
828      * @param newMailboxId ID of the new mailbox that the messages will be moved to
829      * @return an asynchronous task that executes the move (for testing only)
830      */
moveMessages(final long[] messageIds, final long newMailboxId)831     public EmailAsyncTask<Void, Void, Void> moveMessages(final long[] messageIds,
832             final long newMailboxId) {
833         if (messageIds == null || messageIds.length == 0) {
834             throw new IllegalArgumentException();
835         }
836         return EmailAsyncTask.runAsyncParallel(new Runnable() {
837             public void run() {
838                 Account account = Account.getAccountForMessageId(mProviderContext, messageIds[0]);
839                 if (account != null) {
840                     ContentValues cv = new ContentValues();
841                     cv.put(EmailContent.MessageColumns.MAILBOX_KEY, newMailboxId);
842                     ContentResolver resolver = mProviderContext.getContentResolver();
843                     for (long messageId : messageIds) {
844                         Uri uri = ContentUris.withAppendedId(
845                                 EmailContent.Message.SYNCED_CONTENT_URI, messageId);
846                         resolver.update(uri, cv, null, null);
847                     }
848                     if (isMessagingController(account)) {
849                         mLegacyController.processPendingActions(account.mId);
850                     }
851                 }
852             }
853         });
854     }
855 
856     /**
857      * Set/clear the unread status of a message
858      *
859      * @param messageId the message to update
860      * @param isRead the new value for the isRead flag
861      */
862     public void setMessageReadSync(long messageId, boolean isRead) {
863         setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_READ, isRead);
864     }
865 
866     /**
867      * Set/clear the unread status of a message from UI thread
868      *
869      * @param messageId the message to update
870      * @param isRead the new value for the isRead flag
871      * @return the EmailAsyncTask created
872      */
873     public EmailAsyncTask<Void, Void, Void> setMessageRead(final long messageId,
874             final boolean isRead) {
875         return EmailAsyncTask.runAsyncParallel(new Runnable() {
876             @Override
877             public void run() {
878                 setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_READ, isRead);
879             }});
880     }
881 
882     /**
883      * Update a message record and ping MessagingController, if necessary
884      *
885      * @param messageId the message to update
886      * @param cv the ContentValues used in the update
887      */
888     private void updateMessageSync(long messageId, ContentValues cv) {
889         Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
890         mProviderContext.getContentResolver().update(uri, cv, null, null);
891 
892         // Service runs automatically, MessagingController needs a kick
893         long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
894         if (accountId == Account.NO_ACCOUNT) return;
895         if (isMessagingController(accountId)) {
896             mLegacyController.processPendingActions(accountId);
897         }
898     }
899 
900     /**
901      * Set the answered status of a message
902      *
903      * @param messageId the message to update
904      * @return the AsyncTask that will execute the changes (for testing only)
905      */
906     public void setMessageAnsweredOrForwarded(final long messageId,
907             final int flag) {
908         EmailAsyncTask.runAsyncParallel(new Runnable() {
909             public void run() {
910                 Message msg = Message.restoreMessageWithId(mProviderContext, messageId);
911                 if (msg == null) {
912                     Log.w(Logging.LOG_TAG, "Unable to find source message for a reply/forward");
913                     return;
914                 }
915                 ContentValues cv = new ContentValues();
916                 cv.put(MessageColumns.FLAGS, msg.mFlags | flag);
917                 updateMessageSync(messageId, cv);
918             }
919         });
920     }
921 
922     /**
923      * Set/clear the favorite status of a message from UI thread
924      *
925      * @param messageId the message to update
926      * @param isFavorite the new value for the isFavorite flag
927      * @return the EmailAsyncTask created
928      */
929     public EmailAsyncTask<Void, Void, Void> setMessageFavorite(final long messageId,
930             final boolean isFavorite) {
931         return EmailAsyncTask.runAsyncParallel(new Runnable() {
932             @Override
933             public void run() {
934                 setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_FAVORITE,
935                         isFavorite);
936             }});
937     }
938     /**
939      * Set/clear the favorite status of a message
940      *
941      * @param messageId the message to update
942      * @param isFavorite the new value for the isFavorite flag
943      */
944     public void setMessageFavoriteSync(long messageId, boolean isFavorite) {
945         setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite);
946     }
947 
948     /**
949      * Set/clear boolean columns of a message
950      *
951      * @param messageId the message to update
952      * @param columnName the column to update
953      * @param columnValue the new value for the column
954      */
955     private void setMessageBooleanSync(long messageId, String columnName, boolean columnValue) {
956         ContentValues cv = new ContentValues();
957         cv.put(columnName, columnValue);
958         updateMessageSync(messageId, cv);
959     }
960 
961 
962     private static final HashMap<Long, SearchParams> sSearchParamsMap =
963         new HashMap<Long, SearchParams>();
964 
965     public void searchMore(long accountId) throws MessagingException {
966         SearchParams params = sSearchParamsMap.get(accountId);
967         if (params == null) return;
968         params.mOffset += params.mLimit;
969         searchMessages(accountId, params);
970     }
971 
972     /**
973      * Search for messages on the (IMAP) server; do not call this on the UI thread!
974      * @param accountId the id of the account to be searched
975      * @param searchParams the parameters for this search
976      * @throws MessagingException
977      */
978     public int searchMessages(final long accountId, final SearchParams searchParams)
979             throws MessagingException {
980         // Find/create our search mailbox
981         Mailbox searchMailbox = getSearchMailbox(accountId);
982         if (searchMailbox == null) return 0;
983         final long searchMailboxId = searchMailbox.mId;
984         // Save this away (per account)
985         sSearchParamsMap.put(accountId, searchParams);
986 
987         if (searchParams.mOffset == 0) {
988             // Delete existing contents of search mailbox
989             ContentResolver resolver = mContext.getContentResolver();
990             resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId,
991                     null);
992             ContentValues cv = new ContentValues();
993             // For now, use the actual query as the name of the mailbox
994             cv.put(Mailbox.DISPLAY_NAME, searchParams.mFilter);
995             resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId),
996                     cv, null, null);
997         }
998 
999         IEmailService service = getServiceForAccount(accountId);
1000         if (service != null) {
1001             // Service implementation
1002             try {
1003                 return service.searchMessages(accountId, searchParams, searchMailboxId);
1004             } catch (RemoteException e) {
1005                 // TODO Change exception handling to be consistent with however this method
1006                 // is implemented for other protocols
1007                 Log.e("searchMessages", "RemoteException", e);
1008                 return 0;
1009             }
1010         } else {
1011             // This is the actual mailbox we'll be searching
1012             Mailbox actualMailbox = Mailbox.restoreMailboxWithId(mContext, searchParams.mMailboxId);
1013             if (actualMailbox == null) {
1014                 Log.e(Logging.LOG_TAG, "Unable to find mailbox " + searchParams.mMailboxId
1015                         + " to search in with " + searchParams);
1016                 return 0;
1017             }
1018             // Do the search
1019             if (Email.DEBUG) {
1020                 Log.d(Logging.LOG_TAG, "Search: " + searchParams.mFilter);
1021             }
1022             return mLegacyController.searchMailbox(accountId, searchParams, searchMailboxId);
1023         }
1024     }
1025 
1026     /**
1027      * Respond to a meeting invitation.
1028      *
1029      * @param messageId the id of the invitation being responded to
1030      * @param response the code representing the response to the invitation
1031      */
1032     public void sendMeetingResponse(final long messageId, final int response) {
1033          // Split here for target type (Service or MessagingController)
1034         IEmailService service = getServiceForMessage(messageId);
1035         if (service != null) {
1036             // Service implementation
1037             try {
1038                 service.sendMeetingResponse(messageId, response);
1039             } catch (RemoteException e) {
1040                 // TODO Change exception handling to be consistent with however this method
1041                 // is implemented for other protocols
1042                 Log.e("onDownloadAttachment", "RemoteException", e);
1043             }
1044         }
1045     }
1046 
1047     /**
1048      * Request that an attachment be loaded.  It will be stored at a location controlled
1049      * by the AttachmentProvider.
1050      *
1051      * @param attachmentId the attachment to load
1052      * @param messageId the owner message
1053      * @param accountId the owner account
1054      */
1055     public void loadAttachment(final long attachmentId, final long messageId,
1056             final long accountId) {
1057         Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId);
1058         if (attachInfo == null) {
1059             return;
1060         }
1061 
1062         if (Utility.attachmentExists(mProviderContext, attachInfo)) {
1063             // The attachment has already been downloaded, so we will just "pretend" to download it
1064             // This presumably is for POP3 messages
1065             synchronized (mListeners) {
1066                 for (Result listener : mListeners) {
1067                     listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 0);
1068                 }
1069                 for (Result listener : mListeners) {
1070                     listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 100);
1071                 }
1072             }
1073             return;
1074         }
1075 
1076         // Flag the attachment as needing download at the user's request
1077         ContentValues cv = new ContentValues();
1078         cv.put(Attachment.FLAGS, attachInfo.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST);
1079         attachInfo.update(mProviderContext, cv);
1080     }
1081 
1082     /**
1083      * For a given message id, return a service proxy if applicable, or null.
1084      *
1085      * @param messageId the message of interest
1086      * @result service proxy, or null if n/a
1087      */
1088     private IEmailService getServiceForMessage(long messageId) {
1089         // TODO make this more efficient, caching the account, smaller lookup here, etc.
1090         Message message = Message.restoreMessageWithId(mProviderContext, messageId);
1091         if (message == null) {
1092             return null;
1093         }
1094         return getServiceForAccount(message.mAccountKey);
1095     }
1096 
1097     /**
1098      * For a given account id, return a service proxy if applicable, or null.
1099      *
1100      * @param accountId the message of interest
1101      * @result service proxy, or null if n/a
1102      */
1103     private IEmailService getServiceForAccount(long accountId) {
1104         if (isMessagingController(accountId)) return null;
1105         return getExchangeEmailService();
1106     }
1107 
1108     private IEmailService getExchangeEmailService() {
1109         return EmailServiceUtils.getExchangeService(mContext, mServiceCallback);
1110     }
1111 
1112     /**
1113      * Simple helper to determine if legacy MessagingController should be used
1114      */
1115     public boolean isMessagingController(Account account) {
1116         if (account == null) return false;
1117         return isMessagingController(account.mId);
1118     }
1119 
1120     public boolean isMessagingController(long accountId) {
1121         Boolean isLegacyController = mLegacyControllerMap.get(accountId);
1122         if (isLegacyController == null) {
1123             String protocol = Account.getProtocol(mProviderContext, accountId);
1124             isLegacyController = ("pop3".equals(protocol) || "imap".equals(protocol));
1125             mLegacyControllerMap.put(accountId, isLegacyController);
1126         }
1127         return isLegacyController;
1128     }
1129 
1130     /**
1131      * Delete an account.
1132      */
1133     public void deleteAccount(final long accountId) {
1134         EmailAsyncTask.runAsyncParallel(new Runnable() {
1135             @Override
1136             public void run() {
1137                 deleteAccountSync(accountId, mProviderContext);
1138             }
1139         });
1140     }
1141 
1142     /**
1143      * Delete an account synchronously.
1144      */
1145     public void deleteAccountSync(long accountId, Context context) {
1146         try {
1147             mLegacyControllerMap.remove(accountId);
1148             // Get the account URI.
1149             final Account account = Account.restoreAccountWithId(context, accountId);
1150             if (account == null) {
1151                 return; // Already deleted?
1152             }
1153 
1154             // Delete account data, attachments, PIM data, etc.
1155             deleteSyncedDataSync(accountId);
1156 
1157             // Now delete the account itself
1158             Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
1159             context.getContentResolver().delete(uri, null, null);
1160 
1161             // For unit tests, don't run backup, security, and ui pieces.
1162             if (mInUnitTests) {
1163                 return;
1164             }
1165 
1166             // Clean up
1167             AccountBackupRestore.backup(context);
1168             SecurityPolicy.getInstance(context).reducePolicies();
1169             Email.setServicesEnabledSync(context);
1170             Email.setNotifyUiAccountsChanged(true);
1171             MailService.actionReschedule(context);
1172         } catch (Exception e) {
1173             Log.w(Logging.LOG_TAG, "Exception while deleting account", e);
1174         }
1175     }
1176 
1177     /**
1178      * Delete all synced data, but don't delete the actual account.  This is used when security
1179      * policy requirements are not met, and we don't want to reveal any synced data, but we do
1180      * wish to keep the account configured (e.g. to accept remote wipe commands).
1181      *
1182      * The only mailbox not deleted is the account mailbox (if any)
1183      * Also, clear the sync keys on the remaining account, since the data is gone.
1184      *
1185      * SYNCHRONOUS - do not call from UI thread.
1186      *
1187      * @param accountId The account to wipe.
1188      */
1189     public void deleteSyncedDataSync(long accountId) {
1190         try {
1191             // Delete synced attachments
1192             AttachmentUtilities.deleteAllAccountAttachmentFiles(mProviderContext,
1193                     accountId);
1194 
1195             // Delete synced email, leaving only an empty inbox.  We do this in two phases:
1196             // 1. Delete all non-inbox mailboxes (which will delete all of their messages)
1197             // 2. Delete all remaining messages (which will be the inbox messages)
1198             ContentResolver resolver = mProviderContext.getContentResolver();
1199             String[] accountIdArgs = new String[] { Long.toString(accountId) };
1200             resolver.delete(Mailbox.CONTENT_URI,
1201                     MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION,
1202                     accountIdArgs);
1203             resolver.delete(Message.CONTENT_URI, MESSAGES_FOR_ACCOUNT_SELECTION, accountIdArgs);
1204 
1205             // Delete sync keys on remaining items
1206             ContentValues cv = new ContentValues();
1207             cv.putNull(Account.SYNC_KEY);
1208             resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs);
1209             cv.clear();
1210             cv.putNull(Mailbox.SYNC_KEY);
1211             resolver.update(Mailbox.CONTENT_URI, cv,
1212                     MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs);
1213 
1214             // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable
1215             IEmailService service = getServiceForAccount(accountId);
1216             if (service != null) {
1217                 service.deleteAccountPIMData(accountId);
1218             }
1219         } catch (Exception e) {
1220             Log.w(Logging.LOG_TAG, "Exception while deleting account synced data", e);
1221         }
1222     }
1223 
1224     /**
1225      * Simple callback for synchronous commands.  For many commands, this can be largely ignored
1226      * and the result is observed via provider cursors.  The callback will *not* necessarily be
1227      * made from the UI thread, so you may need further handlers to safely make UI updates.
1228      */
1229     public static abstract class Result {
1230         private volatile boolean mRegistered;
1231 
1232         protected void setRegistered(boolean registered) {
1233             mRegistered = registered;
1234         }
1235 
1236         protected final boolean isRegistered() {
1237             return mRegistered;
1238         }
1239 
1240         /**
1241          * Callback for updateMailboxList
1242          *
1243          * @param result If null, the operation completed without error
1244          * @param accountId The account being operated on
1245          * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
1246          */
1247         public void updateMailboxListCallback(MessagingException result, long accountId,
1248                 int progress) {
1249         }
1250 
1251         /**
1252          * Callback for updateMailbox.  Note:  This looks a lot like checkMailCallback, but
1253          * it's a separate call used only by UI's, so we can keep things separate.
1254          *
1255          * @param result If null, the operation completed without error
1256          * @param accountId The account being operated on
1257          * @param mailboxId The mailbox being operated on
1258          * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
1259          * @param numNewMessages the number of new messages delivered
1260          */
1261         public void updateMailboxCallback(MessagingException result, long accountId,
1262                 long mailboxId, int progress, int numNewMessages, ArrayList<Long> addedMessages) {
1263         }
1264 
1265         /**
1266          * Callback for loadMessageForView
1267          *
1268          * @param result if null, the attachment completed - if non-null, terminating with failure
1269          * @param messageId the message which contains the attachment
1270          * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
1271          */
1272         public void loadMessageForViewCallback(MessagingException result, long accountId,
1273                 long messageId, int progress) {
1274         }
1275 
1276         /**
1277          * Callback for loadAttachment
1278          *
1279          * @param result if null, the attachment completed - if non-null, terminating with failure
1280          * @param messageId the message which contains the attachment
1281          * @param attachmentId the attachment being loaded
1282          * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
1283          */
1284         public void loadAttachmentCallback(MessagingException result, long accountId,
1285                 long messageId, long attachmentId, int progress) {
1286         }
1287 
1288         /**
1289          * Callback for checkmail.  Note:  This looks a lot like updateMailboxCallback, but
1290          * it's a separate call used only by the automatic checker service, so we can keep
1291          * things separate.
1292          *
1293          * @param result If null, the operation completed without error
1294          * @param accountId The account being operated on
1295          * @param mailboxId The mailbox being operated on (may be unknown at start)
1296          * @param progress 0 for "starting", no updates, 100 for complete
1297          * @param tag the same tag that was passed to serviceCheckMail()
1298          */
1299         public void serviceCheckMailCallback(MessagingException result, long accountId,
1300                 long mailboxId, int progress, long tag) {
1301         }
1302 
1303         /**
1304          * Callback for sending pending messages.  This will be called once to start the
1305          * group, multiple times for messages, and once to complete the group.
1306          *
1307          * Unfortunately this callback works differently on SMTP and EAS.
1308          *
1309          * On SMTP:
1310          *
1311          * First, we get this.
1312          *  result == null, messageId == -1, progress == 0:     start batch send
1313          *
1314          * Then we get these callbacks per message.
1315          * (Exchange backend may skip "start sending one message".)
1316          *  result == null, messageId == xx, progress == 0:     start sending one message
1317          *  result == xxxx, messageId == xx, progress == 0;     failed sending one message
1318          *
1319          * Finally we get this.
1320          *  result == null, messageId == -1, progres == 100;    finish sending batch
1321          *
1322          * On EAS: Almost same as above, except:
1323          *
1324          * - There's no first ("start batch send") callback.
1325          * - accountId is always -1.
1326          *
1327          * @param result If null, the operation completed without error
1328          * @param accountId The account being operated on
1329          * @param messageId The being sent (may be unknown at start)
1330          * @param progress 0 for "starting", 100 for complete
1331          */
1332         public void sendMailCallback(MessagingException result, long accountId,
1333                 long messageId, int progress) {
1334         }
1335     }
1336 
1337     /**
1338      * Bridge to intercept {@link MessageRetrievalListener#loadAttachmentProgress} and
1339      * pass down to {@link Result}.
1340      */
1341     public class MessageRetrievalListenerBridge implements MessageRetrievalListener {
1342         private final long mMessageId;
1343         private final long mAttachmentId;
1344         private final long mAccountId;
1345 
1346         public MessageRetrievalListenerBridge(long messageId, long attachmentId) {
1347             mMessageId = messageId;
1348             mAttachmentId = attachmentId;
1349             mAccountId = Account.getAccountIdForMessageId(mProviderContext, mMessageId);
1350         }
1351 
1352         @Override
1353         public void loadAttachmentProgress(int progress) {
1354               synchronized (mListeners) {
1355                   for (Result listener : mListeners) {
1356                       listener.loadAttachmentCallback(null, mAccountId, mMessageId, mAttachmentId,
1357                               progress);
1358                  }
1359               }
1360         }
1361 
1362         @Override
1363         public void messageRetrieved(com.android.emailcommon.mail.Message message) {
1364         }
1365     }
1366 
1367     /**
1368      * Support for receiving callbacks from MessagingController and dealing with UI going
1369      * out of scope.
1370      */
1371     public class LegacyListener extends MessagingListener {
1372         public LegacyListener() {
1373         }
1374 
1375         @Override
1376         public void listFoldersStarted(long accountId) {
1377             synchronized (mListeners) {
1378                 for (Result l : mListeners) {
1379                     l.updateMailboxListCallback(null, accountId, 0);
1380                 }
1381             }
1382         }
1383 
1384         @Override
1385         public void listFoldersFailed(long accountId, String message) {
1386             synchronized (mListeners) {
1387                 for (Result l : mListeners) {
1388                     l.updateMailboxListCallback(new MessagingException(message), accountId, 0);
1389                 }
1390             }
1391         }
1392 
1393         @Override
1394         public void listFoldersFinished(long accountId) {
1395             synchronized (mListeners) {
1396                 for (Result l : mListeners) {
1397                     l.updateMailboxListCallback(null, accountId, 100);
1398                 }
1399             }
1400         }
1401 
1402         @Override
1403         public void synchronizeMailboxStarted(long accountId, long mailboxId) {
1404             synchronized (mListeners) {
1405                 for (Result l : mListeners) {
1406                     l.updateMailboxCallback(null, accountId, mailboxId, 0, 0, null);
1407                 }
1408             }
1409         }
1410 
1411         @Override
1412         public void synchronizeMailboxFinished(long accountId, long mailboxId,
1413                 int totalMessagesInMailbox, int numNewMessages, ArrayList<Long> addedMessages) {
1414             synchronized (mListeners) {
1415                 for (Result l : mListeners) {
1416                     l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages,
1417                             addedMessages);
1418                 }
1419             }
1420         }
1421 
1422         @Override
1423         public void synchronizeMailboxFailed(long accountId, long mailboxId, Exception e) {
1424             MessagingException me;
1425             if (e instanceof MessagingException) {
1426                 me = (MessagingException) e;
1427             } else {
1428                 me = new MessagingException(e.toString());
1429             }
1430             synchronized (mListeners) {
1431                 for (Result l : mListeners) {
1432                     l.updateMailboxCallback(me, accountId, mailboxId, 0, 0, null);
1433                 }
1434             }
1435         }
1436 
1437         @Override
1438         public void checkMailStarted(Context context, long accountId, long tag) {
1439             synchronized (mListeners) {
1440                 for (Result l : mListeners) {
1441                     l.serviceCheckMailCallback(null, accountId, -1, 0, tag);
1442                 }
1443             }
1444         }
1445 
1446         @Override
1447         public void checkMailFinished(Context context, long accountId, long folderId, long tag) {
1448             synchronized (mListeners) {
1449                 for (Result l : mListeners) {
1450                     l.serviceCheckMailCallback(null, accountId, folderId, 100, tag);
1451                 }
1452             }
1453         }
1454 
1455         @Override
1456         public void loadMessageForViewStarted(long messageId) {
1457             final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
1458             synchronized (mListeners) {
1459                 for (Result listener : mListeners) {
1460                     listener.loadMessageForViewCallback(null, accountId, messageId, 0);
1461                 }
1462             }
1463         }
1464 
1465         @Override
1466         public void loadMessageForViewFinished(long messageId) {
1467             final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
1468             synchronized (mListeners) {
1469                 for (Result listener : mListeners) {
1470                     listener.loadMessageForViewCallback(null, accountId, messageId, 100);
1471                 }
1472             }
1473         }
1474 
1475         @Override
1476         public void loadMessageForViewFailed(long messageId, String message) {
1477             final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
1478             synchronized (mListeners) {
1479                 for (Result listener : mListeners) {
1480                     listener.loadMessageForViewCallback(new MessagingException(message),
1481                             accountId, messageId, 0);
1482                 }
1483             }
1484         }
1485 
1486         @Override
1487         public void loadAttachmentStarted(long accountId, long messageId, long attachmentId,
1488                 boolean requiresDownload) {
1489             try {
1490                 mCallbackProxy.loadAttachmentStatus(messageId, attachmentId,
1491                         EmailServiceStatus.IN_PROGRESS, 0);
1492             } catch (RemoteException e) {
1493             }
1494             synchronized (mListeners) {
1495                 for (Result listener : mListeners) {
1496                     listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 0);
1497                 }
1498             }
1499         }
1500 
1501         @Override
1502         public void loadAttachmentFinished(long accountId, long messageId, long attachmentId) {
1503             try {
1504                 mCallbackProxy.loadAttachmentStatus(messageId, attachmentId,
1505                         EmailServiceStatus.SUCCESS, 100);
1506             } catch (RemoteException e) {
1507             }
1508             synchronized (mListeners) {
1509                 for (Result listener : mListeners) {
1510                     listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 100);
1511                 }
1512             }
1513         }
1514 
1515         @Override
1516         public void loadAttachmentFailed(long accountId, long messageId, long attachmentId,
1517                 MessagingException me, boolean background) {
1518             try {
1519                 // If the cause of the MessagingException is an IOException, we send a status of
1520                 // CONNECTION_ERROR; in this case, AttachmentDownloadService will try again to
1521                 // download the attachment.  Otherwise, the error is considered non-recoverable.
1522                 int status = EmailServiceStatus.ATTACHMENT_NOT_FOUND;
1523                 if (me != null && me.getCause() instanceof IOException) {
1524                     status = EmailServiceStatus.CONNECTION_ERROR;
1525                 }
1526                 mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, status, 0);
1527             } catch (RemoteException e) {
1528             }
1529             synchronized (mListeners) {
1530                 for (Result listener : mListeners) {
1531                     // TODO We are overloading the exception here. The UI listens for this
1532                     // callback and displays a toast if the exception is not null. Since we
1533                     // want to avoid displaying toast for background operations, we force
1534                     // the exception to be null. This needs to be re-worked so the UI will
1535                     // only receive (or at least pays attention to) responses for requests
1536                     // it explicitly cares about. Then we would not need to overload the
1537                     // exception parameter.
1538                     listener.loadAttachmentCallback(background ? null : me, accountId, messageId,
1539                             attachmentId, 0);
1540                 }
1541             }
1542         }
1543 
1544         @Override
1545         synchronized public void sendPendingMessagesStarted(long accountId, long messageId) {
1546             synchronized (mListeners) {
1547                 for (Result listener : mListeners) {
1548                     listener.sendMailCallback(null, accountId, messageId, 0);
1549                 }
1550             }
1551         }
1552 
1553         @Override
1554         synchronized public void sendPendingMessagesCompleted(long accountId) {
1555             synchronized (mListeners) {
1556                 for (Result listener : mListeners) {
1557                     listener.sendMailCallback(null, accountId, -1, 100);
1558                 }
1559             }
1560         }
1561 
1562         @Override
1563         synchronized public void sendPendingMessagesFailed(long accountId, long messageId,
1564                 Exception reason) {
1565             MessagingException me;
1566             if (reason instanceof MessagingException) {
1567                 me = (MessagingException) reason;
1568             } else {
1569                 me = new MessagingException(reason.toString());
1570             }
1571             synchronized (mListeners) {
1572                 for (Result listener : mListeners) {
1573                     listener.sendMailCallback(me, accountId, messageId, 0);
1574                 }
1575             }
1576         }
1577     }
1578 
1579     /**
1580      * Service callback for service operations
1581      */
1582     private class ServiceCallback extends IEmailServiceCallback.Stub {
1583 
1584         private final static boolean DEBUG_FAIL_DOWNLOADS = false;       // do not check in "true"
1585 
1586         public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
1587                 int progress) {
1588             MessagingException result = mapStatusToException(statusCode);
1589             switch (statusCode) {
1590                 case EmailServiceStatus.SUCCESS:
1591                     progress = 100;
1592                     break;
1593                 case EmailServiceStatus.IN_PROGRESS:
1594                     if (DEBUG_FAIL_DOWNLOADS && progress > 75) {
1595                         result = new MessagingException(
1596                                 String.valueOf(EmailServiceStatus.CONNECTION_ERROR));
1597                     }
1598                     // discard progress reports that look like sentinels
1599                     if (progress < 0 || progress >= 100) {
1600                         return;
1601                     }
1602                     break;
1603             }
1604             final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
1605             synchronized (mListeners) {
1606                 for (Result listener : mListeners) {
1607                     listener.loadAttachmentCallback(result, accountId, messageId, attachmentId,
1608                             progress);
1609                 }
1610             }
1611         }
1612 
1613         /**
1614          * Note, this is an incomplete implementation of this callback, because we are
1615          * not getting things back from Service in quite the same way as from MessagingController.
1616          * However, this is sufficient for basic "progress=100" notification that message send
1617          * has just completed.
1618          */
1619         public void sendMessageStatus(long messageId, String subject, int statusCode,
1620                 int progress) {
1621             long accountId = -1;        // This should be in the callback
1622             MessagingException result = mapStatusToException(statusCode);
1623             switch (statusCode) {
1624                 case EmailServiceStatus.SUCCESS:
1625                     progress = 100;
1626                     break;
1627                 case EmailServiceStatus.IN_PROGRESS:
1628                     // discard progress reports that look like sentinels
1629                     if (progress < 0 || progress >= 100) {
1630                         return;
1631                     }
1632                     break;
1633             }
1634             synchronized(mListeners) {
1635                 for (Result listener : mListeners) {
1636                     listener.sendMailCallback(result, accountId, messageId, progress);
1637                 }
1638             }
1639         }
1640 
1641         public void syncMailboxListStatus(long accountId, int statusCode, int progress) {
1642             MessagingException result = mapStatusToException(statusCode);
1643             switch (statusCode) {
1644                 case EmailServiceStatus.SUCCESS:
1645                     progress = 100;
1646                     break;
1647                 case EmailServiceStatus.IN_PROGRESS:
1648                     // discard progress reports that look like sentinels
1649                     if (progress < 0 || progress >= 100) {
1650                         return;
1651                     }
1652                     break;
1653             }
1654             synchronized(mListeners) {
1655                 for (Result listener : mListeners) {
1656                     listener.updateMailboxListCallback(result, accountId, progress);
1657                 }
1658             }
1659         }
1660 
1661         public void syncMailboxStatus(long mailboxId, int statusCode, int progress) {
1662             MessagingException result = mapStatusToException(statusCode);
1663             switch (statusCode) {
1664                 case EmailServiceStatus.SUCCESS:
1665                     progress = 100;
1666                     break;
1667                 case EmailServiceStatus.IN_PROGRESS:
1668                     // discard progress reports that look like sentinels
1669                     if (progress < 0 || progress >= 100) {
1670                         return;
1671                     }
1672                     break;
1673             }
1674             // TODO should pass this back instead of looking it up here
1675             Mailbox mbx = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
1676             // The mailbox could have disappeared if the server commanded it
1677             if (mbx == null) return;
1678             long accountId = mbx.mAccountKey;
1679             synchronized(mListeners) {
1680                 for (Result listener : mListeners) {
1681                     listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0, null);
1682                 }
1683             }
1684         }
1685 
1686         private MessagingException mapStatusToException(int statusCode) {
1687             switch (statusCode) {
1688                 case EmailServiceStatus.SUCCESS:
1689                 case EmailServiceStatus.IN_PROGRESS:
1690                 // Don't generate error if the account is uninitialized
1691                 case EmailServiceStatus.ACCOUNT_UNINITIALIZED:
1692                     return null;
1693 
1694                 case EmailServiceStatus.LOGIN_FAILED:
1695                     return new AuthenticationFailedException("");
1696 
1697                 case EmailServiceStatus.CONNECTION_ERROR:
1698                     return new MessagingException(MessagingException.IOERROR);
1699 
1700                 case EmailServiceStatus.SECURITY_FAILURE:
1701                     return new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
1702 
1703                 case EmailServiceStatus.ACCESS_DENIED:
1704                     return new MessagingException(MessagingException.ACCESS_DENIED);
1705 
1706                 case EmailServiceStatus.ATTACHMENT_NOT_FOUND:
1707                     return new MessagingException(MessagingException.ATTACHMENT_NOT_FOUND);
1708 
1709                 case EmailServiceStatus.CLIENT_CERTIFICATE_ERROR:
1710                     return new MessagingException(MessagingException.CLIENT_CERTIFICATE_ERROR);
1711 
1712                 case EmailServiceStatus.MESSAGE_NOT_FOUND:
1713                 case EmailServiceStatus.FOLDER_NOT_DELETED:
1714                 case EmailServiceStatus.FOLDER_NOT_RENAMED:
1715                 case EmailServiceStatus.FOLDER_NOT_CREATED:
1716                 case EmailServiceStatus.REMOTE_EXCEPTION:
1717                     // TODO: define exception code(s) & UI string(s) for server-side errors
1718                 default:
1719                     return new MessagingException(String.valueOf(statusCode));
1720             }
1721         }
1722 
1723         @Override
1724         public void loadMessageStatus(long messageId, int statusCode, int progress)
1725                 throws RemoteException {
1726         }
1727     }
1728 
1729     private interface ServiceCallbackWrapper {
1730         public void call(IEmailServiceCallback cb) throws RemoteException;
1731     }
1732 
1733     /**
1734      * Proxy that can be used to broadcast service callbacks; we currently use this only for
1735      * loadAttachment callbacks
1736      */
1737     private final IEmailServiceCallback.Stub mCallbackProxy =
1738         new IEmailServiceCallback.Stub() {
1739 
1740         /**
1741          * Broadcast a callback to the everyone that's registered
1742          *
1743          * @param wrapper the ServiceCallbackWrapper used in the broadcast
1744          */
1745         private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) {
1746             if (sCallbackList != null) {
1747                 // Call everyone on our callback list
1748                 // Exceptions can be safely ignored
1749                 int count = sCallbackList.beginBroadcast();
1750                 for (int i = 0; i < count; i++) {
1751                     try {
1752                         wrapper.call(sCallbackList.getBroadcastItem(i));
1753                     } catch (RemoteException e) {
1754                     }
1755                 }
1756                 sCallbackList.finishBroadcast();
1757             }
1758         }
1759 
1760         public void loadAttachmentStatus(final long messageId, final long attachmentId,
1761                 final int status, final int progress) {
1762             broadcastCallback(new ServiceCallbackWrapper() {
1763                 @Override
1764                 public void call(IEmailServiceCallback cb) throws RemoteException {
1765                     cb.loadAttachmentStatus(messageId, attachmentId, status, progress);
1766                 }
1767             });
1768         }
1769 
1770         @Override
1771         public void sendMessageStatus(long messageId, String subject, int statusCode, int progress){
1772         }
1773 
1774         @Override
1775         public void syncMailboxListStatus(long accountId, int statusCode, int progress) {
1776         }
1777 
1778         @Override
1779         public void syncMailboxStatus(long mailboxId, int statusCode, int progress) {
1780         }
1781 
1782         @Override
1783         public void loadMessageStatus(long messageId, int statusCode, int progress)
1784                 throws RemoteException {
1785         }
1786     };
1787 
1788     public static class ControllerService extends Service {
1789         /**
1790          * Create our EmailService implementation here.  For now, only loadAttachment is supported;
1791          * the intention, however, is to move more functionality to the service interface
1792          */
1793         private final IEmailService.Stub mBinder = new IEmailService.Stub() {
1794 
1795             public Bundle validate(HostAuth hostAuth) {
1796                 return null;
1797             }
1798 
1799             public Bundle autoDiscover(String userName, String password) {
1800                 return null;
1801             }
1802 
1803             public void startSync(long mailboxId, boolean userRequest) {
1804             }
1805 
1806             public void stopSync(long mailboxId) {
1807             }
1808 
1809             public void loadAttachment(long attachmentId, boolean background)
1810                     throws RemoteException {
1811                 Attachment att = Attachment.restoreAttachmentWithId(ControllerService.this,
1812                         attachmentId);
1813                 if (att != null) {
1814                     if (Email.DEBUG) {
1815                         Log.d(TAG, "loadAttachment " + attachmentId + ": " + att.mFileName);
1816                     }
1817                     Message msg = Message.restoreMessageWithId(ControllerService.this,
1818                             att.mMessageKey);
1819                     if (msg != null) {
1820                         // If the message is a forward and the attachment needs downloading, we need
1821                         // to retrieve the message from the source, rather than from the message
1822                         // itself
1823                         if ((msg.mFlags & Message.FLAG_TYPE_FORWARD) != 0) {
1824                             String[] cols = Utility.getRowColumns(ControllerService.this,
1825                                     Body.CONTENT_URI, BODY_SOURCE_KEY_PROJECTION, WHERE_MESSAGE_KEY,
1826                                     new String[] {Long.toString(msg.mId)});
1827                             if (cols != null) {
1828                                 msg = Message.restoreMessageWithId(ControllerService.this,
1829                                         Long.parseLong(cols[BODY_SOURCE_KEY_COLUMN]));
1830                                 if (msg == null) {
1831                                     // TODO: We can try restoring from the deleted table here...
1832                                     return;
1833                                 }
1834                             }
1835                         }
1836                         MessagingController legacyController = sInstance.mLegacyController;
1837                         LegacyListener legacyListener = sInstance.mLegacyListener;
1838                         legacyController.loadAttachment(msg.mAccountKey, msg.mId, msg.mMailboxKey,
1839                                 attachmentId, legacyListener, background);
1840                     } else {
1841                         // Send back the specific error status for this case
1842                         sInstance.mCallbackProxy.loadAttachmentStatus(att.mMessageKey, attachmentId,
1843                                 EmailServiceStatus.MESSAGE_NOT_FOUND, 0);
1844                     }
1845                 }
1846             }
1847 
1848             public void updateFolderList(long accountId) {
1849             }
1850 
1851             public void hostChanged(long accountId) {
1852             }
1853 
1854             public void setLogging(int flags) {
1855             }
1856 
1857             public void sendMeetingResponse(long messageId, int response) {
1858             }
1859 
1860             public void loadMore(long messageId) {
1861             }
1862 
1863             // The following three methods are not implemented in this version
1864             public boolean createFolder(long accountId, String name) {
1865                 return false;
1866             }
1867 
1868             public boolean deleteFolder(long accountId, String name) {
1869                 return false;
1870             }
1871 
1872             public boolean renameFolder(long accountId, String oldName, String newName) {
1873                 return false;
1874             }
1875 
1876             public void setCallback(IEmailServiceCallback cb) {
1877                 sCallbackList.register(cb);
1878             }
1879 
1880             public void deleteAccountPIMData(long accountId) {
1881             }
1882 
1883             public int searchMessages(long accountId, SearchParams searchParams,
1884                     long destMailboxId) {
1885                 return 0;
1886             }
1887 
1888             @Override
1889             public int getApiLevel() {
1890                 return Api.LEVEL;
1891             }
1892 
1893             @Override
1894             public void sendMail(long accountId) throws RemoteException {
1895             }
1896         };
1897 
1898         @Override
1899         public IBinder onBind(Intent intent) {
1900             return mBinder;
1901         }
1902     }
1903 }
1904