• 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 com.android.email.mail.AuthenticationFailedException;
20 import com.android.email.mail.MessagingException;
21 import com.android.email.mail.Store;
22 import com.android.email.provider.AttachmentProvider;
23 import com.android.email.provider.EmailContent;
24 import com.android.email.provider.EmailContent.Account;
25 import com.android.email.provider.EmailContent.Attachment;
26 import com.android.email.provider.EmailContent.Mailbox;
27 import com.android.email.provider.EmailContent.MailboxColumns;
28 import com.android.email.provider.EmailContent.Message;
29 import com.android.email.provider.EmailContent.MessageColumns;
30 import com.android.email.service.EmailServiceStatus;
31 import com.android.email.service.IEmailService;
32 import com.android.email.service.IEmailServiceCallback;
33 
34 import android.content.ContentResolver;
35 import android.content.ContentUris;
36 import android.content.ContentValues;
37 import android.content.Context;
38 import android.database.Cursor;
39 import android.net.Uri;
40 import android.os.RemoteException;
41 import android.util.Log;
42 
43 import java.io.File;
44 import java.util.HashSet;
45 
46 /**
47  * New central controller/dispatcher for Email activities that may require remote operations.
48  * Handles disambiguating between legacy MessagingController operations and newer provider/sync
49  * based code.
50  */
51 public class Controller {
52 
53     private static Controller sInstance;
54     private final Context mContext;
55     private Context mProviderContext;
56     private final MessagingController mLegacyController;
57     private final LegacyListener mLegacyListener = new LegacyListener();
58     private final ServiceCallback mServiceCallback = new ServiceCallback();
59     private final HashSet<Result> mListeners = new HashSet<Result>();
60 
61     private static String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] {
62         EmailContent.RECORD_ID,
63         EmailContent.MessageColumns.ACCOUNT_KEY
64     };
65     private static int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1;
66 
67     private static String[] MESSAGEID_TO_MAILBOXID_PROJECTION = new String[] {
68         EmailContent.RECORD_ID,
69         EmailContent.MessageColumns.MAILBOX_KEY
70     };
71     private static int MESSAGEID_TO_MAILBOXID_COLUMN_MAILBOXID = 1;
72 
Controller(Context _context)73     protected Controller(Context _context) {
74         mContext = _context;
75         mProviderContext = _context;
76         mLegacyController = MessagingController.getInstance(mContext);
77         mLegacyController.addListener(mLegacyListener);
78     }
79 
80     /**
81      * Gets or creates the singleton instance of Controller.
82      * @param _context The context that will be used for all underlying system access
83      */
getInstance(Context _context)84     public synchronized static Controller getInstance(Context _context) {
85         if (sInstance == null) {
86             sInstance = new Controller(_context);
87         }
88         return sInstance;
89     }
90 
91     /**
92      * For testing only:  Inject a different context for provider access.  This will be
93      * used internally for access the underlying provider (e.g. getContentResolver().query()).
94      * @param providerContext the provider context to be used by this instance
95      */
setProviderContext(Context providerContext)96     public void setProviderContext(Context providerContext) {
97         mProviderContext = providerContext;
98     }
99 
100     /**
101      * Any UI code that wishes for callback results (on async ops) should register their callback
102      * here (typically from onResume()).  Unregistered callbacks will never be called, to prevent
103      * problems when the command completes and the activity has already paused or finished.
104      * @param listener The callback that may be used in action methods
105      */
addResultCallback(Result listener)106     public void addResultCallback(Result listener) {
107         synchronized (mListeners) {
108             mListeners.add(listener);
109         }
110     }
111 
112     /**
113      * Any UI code that no longer wishes for callback results (on async ops) should unregister
114      * their callback here (typically from onPause()).  Unregistered callbacks will never be called,
115      * to prevent problems when the command completes and the activity has already paused or
116      * finished.
117      * @param listener The callback that may no longer be used
118      */
removeResultCallback(Result listener)119     public void removeResultCallback(Result listener) {
120         synchronized (mListeners) {
121             mListeners.remove(listener);
122         }
123     }
124 
isActiveResultCallback(Result listener)125     private boolean isActiveResultCallback(Result listener) {
126         synchronized (mListeners) {
127             return mListeners.contains(listener);
128         }
129     }
130 
131     /**
132      * Enable/disable logging for external sync services
133      *
134      * Generally this should be called by anybody who changes Email.DEBUG
135      */
serviceLogging(int debugEnabled)136     public void serviceLogging(int debugEnabled) {
137         IEmailService service = ExchangeUtils.getExchangeEmailService(mContext, mServiceCallback);
138         try {
139             service.setLogging(debugEnabled);
140         } catch (RemoteException e) {
141             // TODO Change exception handling to be consistent with however this method
142             // is implemented for other protocols
143             Log.d("updateMailboxList", "RemoteException" + e);
144         }
145     }
146 
147     /**
148      * Request a remote update of mailboxes for an account.
149      *
150      * TODO: Clean up threading in MessagingController cases (or perhaps here in Controller)
151      */
updateMailboxList(final long accountId, final Result callback)152     public void updateMailboxList(final long accountId, final Result callback) {
153 
154         IEmailService service = getServiceForAccount(accountId);
155         if (service != null) {
156             // Service implementation
157             try {
158                 service.updateFolderList(accountId);
159             } catch (RemoteException e) {
160                 // TODO Change exception handling to be consistent with however this method
161                 // is implemented for other protocols
162                 Log.d("updateMailboxList", "RemoteException" + e);
163             }
164         } else {
165             // MessagingController implementation
166             new Thread() {
167                 @Override
168                 public void run() {
169                     mLegacyController.listFolders(accountId, mLegacyListener);
170                 }
171             }.start();
172         }
173     }
174 
175     /**
176      * Request a remote update of a mailbox.  For use by the timed service.
177      *
178      * Functionally this is quite similar to updateMailbox(), but it's a separate API and
179      * separate callback in order to keep UI callbacks from affecting the service loop.
180      */
serviceCheckMail(final long accountId, final long mailboxId, final long tag, final Result callback)181     public void serviceCheckMail(final long accountId, final long mailboxId, final long tag,
182             final Result callback) {
183         IEmailService service = getServiceForAccount(accountId);
184         if (service != null) {
185             // Service implementation
186 //            try {
187                 // TODO this isn't quite going to work, because we're going to get the
188                 // generic (UI) callbacks and not the ones we need to restart the ol' service.
189                 // service.startSync(mailboxId, tag);
190                 callback.serviceCheckMailCallback(null, accountId, mailboxId, 100, tag);
191 //            } catch (RemoteException e) {
192                 // TODO Change exception handling to be consistent with however this method
193                 // is implemented for other protocols
194 //                Log.d("updateMailbox", "RemoteException" + e);
195 //            }
196         } else {
197             // MessagingController implementation
198             new Thread() {
199                 @Override
200                 public void run() {
201                     mLegacyController.checkMail(accountId, tag, mLegacyListener);
202                 }
203             }.start();
204         }
205     }
206 
207     /**
208      * Request a remote update of a mailbox.
209      *
210      * The contract here should be to try and update the headers ASAP, in order to populate
211      * a simple message list.  We should also at this point queue up a background task of
212      * downloading some/all of the messages in this mailbox, but that should be interruptable.
213      */
updateMailbox(final long accountId, final long mailboxId, final Result callback)214     public void updateMailbox(final long accountId, final long mailboxId, final Result callback) {
215 
216         IEmailService service = getServiceForAccount(accountId);
217         if (service != null) {
218             // Service implementation
219             try {
220                 service.startSync(mailboxId);
221             } catch (RemoteException e) {
222                 // TODO Change exception handling to be consistent with however this method
223                 // is implemented for other protocols
224                 Log.d("updateMailbox", "RemoteException" + e);
225             }
226         } else {
227             // MessagingController implementation
228             new Thread() {
229                 @Override
230                 public void run() {
231                     // TODO shouldn't be passing fully-build accounts & mailboxes into APIs
232                     Account account =
233                         EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
234                     Mailbox mailbox =
235                         EmailContent.Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
236                     if (account == null || mailbox == null) {
237                         return;
238                     }
239                     mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
240                 }
241             }.start();
242         }
243     }
244 
245     /**
246      * Request that any final work necessary be done, to load a message.
247      *
248      * Note, this assumes that the caller has already checked message.mFlagLoaded and that
249      * additional work is needed.  There is no optimization here for a message which is already
250      * loaded.
251      *
252      * @param messageId the message to load
253      * @param callback the Controller callback by which results will be reported
254      */
loadMessageForView(final long messageId, final Result callback)255     public void loadMessageForView(final long messageId, final Result callback) {
256 
257         // Split here for target type (Service or MessagingController)
258         IEmailService service = getServiceForMessage(messageId);
259         if (service != null) {
260             // There is no service implementation, so we'll just jam the value, log the error,
261             // and get out of here.
262             Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId);
263             ContentValues cv = new ContentValues();
264             cv.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE);
265             mProviderContext.getContentResolver().update(uri, cv, null, null);
266             Log.d(Email.LOG_TAG, "Unexpected loadMessageForView() for service-based message.");
267             synchronized (mListeners) {
268                 for (Result listener : mListeners) {
269                     listener.loadMessageForViewCallback(null, messageId, 100);
270                 }
271             }
272         } else {
273             // MessagingController implementation
274             new Thread() {
275                 @Override
276                 public void run() {
277                     mLegacyController.loadMessageForView(messageId, mLegacyListener);
278                 }
279             }.start();
280         }
281     }
282 
283 
284     /**
285      * Saves the message to a mailbox of given type.
286      * This is a synchronous operation taking place in the same thread as the caller.
287      * Upon return the message.mId is set.
288      * @param message the message (must have the mAccountId set).
289      * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS).
290      */
saveToMailbox(final EmailContent.Message message, final int mailboxType)291     public void saveToMailbox(final EmailContent.Message message, final int mailboxType) {
292         long accountId = message.mAccountKey;
293         long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType);
294         message.mMailboxKey = mailboxId;
295         message.save(mProviderContext);
296     }
297 
298     /**
299      * @param accountId the account id
300      * @param mailboxType the mailbox type (e.g.  EmailContent.Mailbox.TYPE_TRASH)
301      * @return the id of the mailbox. The mailbox is created if not existing.
302      * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative.
303      * Does not validate the input in other ways (e.g. does not verify the existence of account).
304      */
findOrCreateMailboxOfType(long accountId, int mailboxType)305     public long findOrCreateMailboxOfType(long accountId, int mailboxType) {
306         if (accountId < 0 || mailboxType < 0) {
307             return Mailbox.NO_MAILBOX;
308         }
309         long mailboxId =
310             Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType);
311         return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId;
312     }
313 
314     /**
315      * Returns the server-side name for a specific mailbox.
316      *
317      * @param mailboxType the mailbox type
318      * @return the resource string corresponding to the mailbox type, empty if not found.
319      */
getMailboxServerName(int mailboxType)320     /* package */ String getMailboxServerName(int mailboxType) {
321         int resId = -1;
322         switch (mailboxType) {
323             case Mailbox.TYPE_INBOX:
324                 resId = R.string.mailbox_name_server_inbox;
325                 break;
326             case Mailbox.TYPE_OUTBOX:
327                 resId = R.string.mailbox_name_server_outbox;
328                 break;
329             case Mailbox.TYPE_DRAFTS:
330                 resId = R.string.mailbox_name_server_drafts;
331                 break;
332             case Mailbox.TYPE_TRASH:
333                 resId = R.string.mailbox_name_server_trash;
334                 break;
335             case Mailbox.TYPE_SENT:
336                 resId = R.string.mailbox_name_server_sent;
337                 break;
338             case Mailbox.TYPE_JUNK:
339                 resId = R.string.mailbox_name_server_junk;
340                 break;
341         }
342         return resId != -1 ? mContext.getString(resId) : "";
343     }
344 
345     /**
346      * Create a mailbox given the account and mailboxType.
347      * TODO: Does this need to be signaled explicitly to the sync engines?
348      */
createMailbox(long accountId, int mailboxType)349     /* package */ long createMailbox(long accountId, int mailboxType) {
350         if (accountId < 0 || mailboxType < 0) {
351             String mes = "Invalid arguments " + accountId + ' ' + mailboxType;
352             Log.e(Email.LOG_TAG, mes);
353             throw new RuntimeException(mes);
354         }
355         Mailbox box = new Mailbox();
356         box.mAccountKey = accountId;
357         box.mType = mailboxType;
358         box.mSyncInterval = EmailContent.Account.CHECK_INTERVAL_NEVER;
359         box.mFlagVisible = true;
360         box.mDisplayName = getMailboxServerName(mailboxType);
361         box.save(mProviderContext);
362         return box.mId;
363     }
364 
365     /**
366      * Send a message:
367      * - move the message to Outbox (the message is assumed to be in Drafts).
368      * - EAS service will take it from there
369      * - trigger send for POP/IMAP
370      * @param messageId the id of the message to send
371      */
sendMessage(long messageId, long accountId)372     public void sendMessage(long messageId, long accountId) {
373         ContentResolver resolver = mProviderContext.getContentResolver();
374         if (accountId == -1) {
375             accountId = lookupAccountForMessage(messageId);
376         }
377         if (accountId == -1) {
378             // probably the message was not found
379             if (Email.LOGD) {
380                 Email.log("no account found for message " + messageId);
381             }
382             return;
383         }
384 
385         // Move to Outbox
386         long outboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_OUTBOX);
387         ContentValues cv = new ContentValues();
388         cv.put(EmailContent.MessageColumns.MAILBOX_KEY, outboxId);
389 
390         // does this need to be SYNCED_CONTENT_URI instead?
391         Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId);
392         resolver.update(uri, cv, null, null);
393 
394         // Split here for target type (Service or MessagingController)
395         IEmailService service = getServiceForMessage(messageId);
396         if (service != null) {
397             // We just need to be sure the callback is installed, if this is the first call
398             // to the service.
399             try {
400                 service.setCallback(mServiceCallback);
401             } catch (RemoteException re) {
402                 // OK - not a critical callback here
403             }
404         } else {
405             // for IMAP & POP only, (attempt to) send the message now
406             final EmailContent.Account account =
407                     EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
408             if (account == null) {
409                 return;
410             }
411             final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT);
412             new Thread() {
413                 @Override
414                 public void run() {
415                     mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener);
416                 }
417             }.start();
418         }
419     }
420 
421     /**
422      * Try to send all pending messages for a given account
423      *
424      * @param accountId the account for which to send messages (-1 for all accounts)
425      * @param callback
426      */
sendPendingMessages(long accountId, Result callback)427     public void sendPendingMessages(long accountId, Result callback) {
428         // 1. make sure we even have an outbox, exit early if not
429         final long outboxId =
430             Mailbox.findMailboxOfType(mProviderContext, accountId, Mailbox.TYPE_OUTBOX);
431         if (outboxId == Mailbox.NO_MAILBOX) {
432             return;
433         }
434 
435         // 2. dispatch as necessary
436         IEmailService service = getServiceForAccount(accountId);
437         if (service != null) {
438             // Service implementation
439             try {
440                 service.startSync(outboxId);
441             } catch (RemoteException e) {
442                 // TODO Change exception handling to be consistent with however this method
443                 // is implemented for other protocols
444                 Log.d("updateMailbox", "RemoteException" + e);
445             }
446         } else {
447             // MessagingController implementation
448             final EmailContent.Account account =
449                 EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
450             if (account == null) {
451                 return;
452             }
453             final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT);
454             new Thread() {
455                 @Override
456                 public void run() {
457                     mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener);
458                 }
459             }.start();
460         }
461     }
462 
463     /**
464      * Reset visible limits for all accounts.
465      * For each account:
466      *   look up limit
467      *   write limit into all mailboxes for that account
468      */
resetVisibleLimits()469     public void resetVisibleLimits() {
470         new Thread() {
471             @Override
472             public void run() {
473                 ContentResolver resolver = mProviderContext.getContentResolver();
474                 Cursor c = null;
475                 try {
476                     c = resolver.query(
477                             Account.CONTENT_URI,
478                             Account.ID_PROJECTION,
479                             null, null, null);
480                     while (c.moveToNext()) {
481                         long accountId = c.getLong(Account.ID_PROJECTION_COLUMN);
482                         Account account = Account.restoreAccountWithId(mProviderContext, accountId);
483                         if (account != null) {
484                             Store.StoreInfo info = Store.StoreInfo.getStoreInfo(
485                                     account.getStoreUri(mProviderContext), mContext);
486                             if (info != null && info.mVisibleLimitDefault > 0) {
487                                 int limit = info.mVisibleLimitDefault;
488                                 ContentValues cv = new ContentValues();
489                                 cv.put(MailboxColumns.VISIBLE_LIMIT, limit);
490                                 resolver.update(Mailbox.CONTENT_URI, cv,
491                                         MailboxColumns.ACCOUNT_KEY + "=?",
492                                         new String[] { Long.toString(accountId) });
493                             }
494                         }
495                     }
496                 } finally {
497                     if (c != null) {
498                         c.close();
499                     }
500                 }
501             }
502         }.start();
503     }
504 
505     /**
506      * Increase the load count for a given mailbox, and trigger a refresh.  Applies only to
507      * IMAP and POP.
508      *
509      * @param mailboxId the mailbox
510      * @param callback
511      */
loadMoreMessages(final long mailboxId, Result callback)512     public void loadMoreMessages(final long mailboxId, Result callback) {
513         new Thread() {
514             @Override
515             public void run() {
516                 Mailbox mailbox = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
517                 if (mailbox == null) {
518                     return;
519                 }
520                 Account account = Account.restoreAccountWithId(mProviderContext,
521                         mailbox.mAccountKey);
522                 if (account == null) {
523                     return;
524                 }
525                 Store.StoreInfo info = Store.StoreInfo.getStoreInfo(
526                         account.getStoreUri(mProviderContext), mContext);
527                 if (info != null && info.mVisibleLimitIncrement > 0) {
528                     // Use provider math to increment the field
529                     ContentValues cv = new ContentValues();;
530                     cv.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT);
531                     cv.put(EmailContent.ADD_COLUMN_NAME, info.mVisibleLimitIncrement);
532                     Uri uri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, mailboxId);
533                     mProviderContext.getContentResolver().update(uri, cv, null, null);
534                     // Trigger a refresh using the new, longer limit
535                     mailbox.mVisibleLimit += info.mVisibleLimitIncrement;
536                     mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
537                 }
538             }
539         }.start();
540     }
541 
542     /**
543      * @param messageId the id of message
544      * @return the accountId corresponding to the given messageId, or -1 if not found.
545      */
lookupAccountForMessage(long messageId)546     private long lookupAccountForMessage(long messageId) {
547         ContentResolver resolver = mProviderContext.getContentResolver();
548         Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
549                                   MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?",
550                                   new String[] { Long.toString(messageId) }, null);
551         try {
552             return c.moveToFirst()
553                 ? c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID)
554                 : -1;
555         } finally {
556             c.close();
557         }
558     }
559 
560     /**
561      * Delete a single attachment entry from the DB given its id.
562      * Does not delete any eventual associated files.
563      */
deleteAttachment(long attachmentId)564     public void deleteAttachment(long attachmentId) {
565         ContentResolver resolver = mProviderContext.getContentResolver();
566         Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId);
567         resolver.delete(uri, null, null);
568     }
569 
570     /**
571      * Delete a single message by moving it to the trash, or deleting it from the trash
572      *
573      * This function has no callback, no result reporting, because the desired outcome
574      * is reflected entirely by changes to one or more cursors.
575      *
576      * @param messageId The id of the message to "delete".
577      * @param accountId The id of the message's account, or -1 if not known by caller
578      *
579      * TODO: Move out of UI thread
580      * TODO: "get account a for message m" should be a utility
581      * TODO: "get mailbox of type n for account a" should be a utility
582      */
deleteMessage(long messageId, long accountId)583     public void deleteMessage(long messageId, long accountId) {
584         ContentResolver resolver = mProviderContext.getContentResolver();
585 
586         // 1.  Look up acct# for message we're deleting
587         if (accountId == -1) {
588             accountId = lookupAccountForMessage(messageId);
589         }
590         if (accountId == -1) {
591             return;
592         }
593 
594         // 2. Confirm that there is a trash mailbox available.  If not, create one
595         long trashMailboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_TRASH);
596 
597         // 3.  Are we moving to trash or deleting?  It depends on where the message currently sits.
598         long sourceMailboxId = -1;
599         Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
600                 MESSAGEID_TO_MAILBOXID_PROJECTION, EmailContent.RECORD_ID + "=?",
601                 new String[] { Long.toString(messageId) }, null);
602         try {
603             sourceMailboxId = c.moveToFirst()
604                 ? c.getLong(MESSAGEID_TO_MAILBOXID_COLUMN_MAILBOXID)
605                 : -1;
606         } finally {
607             c.close();
608         }
609 
610         // 4.  Drop non-essential data for the message (e.g. attachment files)
611         AttachmentProvider.deleteAllAttachmentFiles(mProviderContext, accountId, messageId);
612 
613         Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
614 
615         // 5. Perform "delete" as appropriate
616         if (sourceMailboxId == trashMailboxId) {
617             // 5a. Delete from trash
618             resolver.delete(uri, null, null);
619         } else {
620             // 5b. Move to trash
621             ContentValues cv = new ContentValues();
622             cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId);
623             resolver.update(uri, cv, null, null);
624         }
625 
626         // 6.  Service runs automatically, MessagingController needs a kick
627         Account account = Account.restoreAccountWithId(mProviderContext, accountId);
628         if (account == null) {
629             return; // isMessagingController returns false for null, but let's make it clear.
630         }
631         if (isMessagingController(account)) {
632             final long syncAccountId = accountId;
633             new Thread() {
634                 @Override
635                 public void run() {
636                     mLegacyController.processPendingActions(syncAccountId);
637                 }
638             }.start();
639         }
640     }
641 
642     /**
643      * Set/clear the unread status of a message
644      *
645      * TODO db ops should not be in this thread. queue it up.
646      *
647      * @param messageId the message to update
648      * @param isRead the new value for the isRead flag
649      */
setMessageRead(final long messageId, boolean isRead)650     public void setMessageRead(final long messageId, boolean isRead) {
651         ContentValues cv = new ContentValues();
652         cv.put(EmailContent.MessageColumns.FLAG_READ, isRead);
653         Uri uri = ContentUris.withAppendedId(
654                 EmailContent.Message.SYNCED_CONTENT_URI, messageId);
655         mProviderContext.getContentResolver().update(uri, cv, null, null);
656 
657         // Service runs automatically, MessagingController needs a kick
658         final Message message = Message.restoreMessageWithId(mProviderContext, messageId);
659         if (message == null) {
660             return;
661         }
662         Account account = Account.restoreAccountWithId(mProviderContext, message.mAccountKey);
663         if (account == null) {
664             return; // isMessagingController returns false for null, but let's make it clear.
665         }
666         if (isMessagingController(account)) {
667             new Thread() {
668                 @Override
669                 public void run() {
670                     mLegacyController.processPendingActions(message.mAccountKey);
671                 }
672             }.start();
673         }
674     }
675 
676     /**
677      * Set/clear the favorite status of a message
678      *
679      * TODO db ops should not be in this thread. queue it up.
680      *
681      * @param messageId the message to update
682      * @param isFavorite the new value for the isFavorite flag
683      */
setMessageFavorite(final long messageId, boolean isFavorite)684     public void setMessageFavorite(final long messageId, boolean isFavorite) {
685         ContentValues cv = new ContentValues();
686         cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite);
687         Uri uri = ContentUris.withAppendedId(
688                 EmailContent.Message.SYNCED_CONTENT_URI, messageId);
689         mProviderContext.getContentResolver().update(uri, cv, null, null);
690 
691         // Service runs automatically, MessagingController needs a kick
692         final Message message = Message.restoreMessageWithId(mProviderContext, messageId);
693         if (message == null) {
694             return;
695         }
696         Account account = Account.restoreAccountWithId(mProviderContext, message.mAccountKey);
697         if (account == null) {
698             return; // isMessagingController returns false for null, but let's make it clear.
699         }
700         if (isMessagingController(account)) {
701             new Thread() {
702                 @Override
703                 public void run() {
704                     mLegacyController.processPendingActions(message.mAccountKey);
705                 }
706             }.start();
707         }
708     }
709 
710     /**
711      * Respond to a meeting invitation.
712      *
713      * @param messageId the id of the invitation being responded to
714      * @param response the code representing the response to the invitation
715      * @callback the Controller callback by which results will be reported (currently not defined)
716      */
sendMeetingResponse(final long messageId, final int response, final Result callback)717     public void sendMeetingResponse(final long messageId, final int response,
718             final Result callback) {
719          // Split here for target type (Service or MessagingController)
720         IEmailService service = getServiceForMessage(messageId);
721         if (service != null) {
722             // Service implementation
723             try {
724                 service.sendMeetingResponse(messageId, response);
725             } catch (RemoteException e) {
726                 // TODO Change exception handling to be consistent with however this method
727                 // is implemented for other protocols
728                 Log.e("onDownloadAttachment", "RemoteException", e);
729             }
730         }
731     }
732 
733     /**
734      * Request that an attachment be loaded.  It will be stored at a location controlled
735      * by the AttachmentProvider.
736      *
737      * @param attachmentId the attachment to load
738      * @param messageId the owner message
739      * @param mailboxId the owner mailbox
740      * @param accountId the owner account
741      * @param callback the Controller callback by which results will be reported
742      */
loadAttachment(final long attachmentId, final long messageId, final long mailboxId, final long accountId, final Result callback)743     public void loadAttachment(final long attachmentId, final long messageId, final long mailboxId,
744             final long accountId, final Result callback) {
745 
746         File saveToFile = AttachmentProvider.getAttachmentFilename(mProviderContext,
747                 accountId, attachmentId);
748         Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId);
749         if (attachInfo == null) {
750             return;
751         }
752 
753         if (saveToFile.exists() && attachInfo.mContentUri != null) {
754             // The attachment has already been downloaded, so we will just "pretend" to download it
755             synchronized (mListeners) {
756                 for (Result listener : mListeners) {
757                     listener.loadAttachmentCallback(null, messageId, attachmentId, 0);
758                 }
759                 for (Result listener : mListeners) {
760                     listener.loadAttachmentCallback(null, messageId, attachmentId, 100);
761                 }
762             }
763             return;
764         }
765 
766         // Split here for target type (Service or MessagingController)
767         IEmailService service = getServiceForMessage(messageId);
768         if (service != null) {
769             // Service implementation
770             try {
771                 service.loadAttachment(attachInfo.mId, saveToFile.getAbsolutePath(),
772                         AttachmentProvider.getAttachmentUri(accountId, attachmentId).toString());
773             } catch (RemoteException e) {
774                 // TODO Change exception handling to be consistent with however this method
775                 // is implemented for other protocols
776                 Log.e("onDownloadAttachment", "RemoteException", e);
777             }
778         } else {
779             // MessagingController implementation
780             new Thread() {
781                 @Override
782                 public void run() {
783                     mLegacyController.loadAttachment(accountId, messageId, mailboxId, attachmentId,
784                             mLegacyListener);
785                 }
786             }.start();
787         }
788     }
789 
790     /**
791      * For a given message id, return a service proxy if applicable, or null.
792      *
793      * @param messageId the message of interest
794      * @result service proxy, or null if n/a
795      */
getServiceForMessage(long messageId)796     private IEmailService getServiceForMessage(long messageId) {
797         // TODO make this more efficient, caching the account, smaller lookup here, etc.
798         Message message = Message.restoreMessageWithId(mProviderContext, messageId);
799         if (message == null) {
800             return null;
801         }
802         return getServiceForAccount(message.mAccountKey);
803     }
804 
805     /**
806      * For a given account id, return a service proxy if applicable, or null.
807      *
808      * TODO this should use a cache because we'll be doing this a lot
809      *
810      * @param accountId the message of interest
811      * @result service proxy, or null if n/a
812      */
getServiceForAccount(long accountId)813     private IEmailService getServiceForAccount(long accountId) {
814         // TODO make this more efficient, caching the account, MUCH smaller lookup here, etc.
815         Account account = EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
816         if (account == null || isMessagingController(account)) {
817             return null;
818         } else {
819             return ExchangeUtils.getExchangeEmailService(mContext, mServiceCallback);
820         }
821     }
822 
823     /**
824      * Simple helper to determine if legacy MessagingController should be used
825      *
826      * TODO this should not require a full account, just an accountId
827      * TODO this should use a cache because we'll be doing this a lot
828      */
isMessagingController(EmailContent.Account account)829     public boolean isMessagingController(EmailContent.Account account) {
830         if (account == null) return false;
831         Store.StoreInfo info =
832             Store.StoreInfo.getStoreInfo(account.getStoreUri(mProviderContext), mContext);
833         // This null happens in testing.
834         if (info == null) {
835             return false;
836         }
837         String scheme = info.mScheme;
838 
839         return ("pop3".equals(scheme) || "imap".equals(scheme));
840     }
841 
842     /**
843      * Simple callback for synchronous commands.  For many commands, this can be largely ignored
844      * and the result is observed via provider cursors.  The callback will *not* necessarily be
845      * made from the UI thread, so you may need further handlers to safely make UI updates.
846      */
847     public interface Result {
848         /**
849          * Callback for updateMailboxList
850          *
851          * @param result If null, the operation completed without error
852          * @param accountId The account being operated on
853          * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
854          */
updateMailboxListCallback(MessagingException result, long accountId, int progress)855         public void updateMailboxListCallback(MessagingException result, long accountId,
856                 int progress);
857 
858         /**
859          * Callback for updateMailbox.  Note:  This looks a lot like checkMailCallback, but
860          * it's a separate call used only by UI's, so we can keep things separate.
861          *
862          * @param result If null, the operation completed without error
863          * @param accountId The account being operated on
864          * @param mailboxId The mailbox being operated on
865          * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
866          * @param numNewMessages the number of new messages delivered
867          */
updateMailboxCallback(MessagingException result, long accountId, long mailboxId, int progress, int numNewMessages)868         public void updateMailboxCallback(MessagingException result, long accountId,
869                 long mailboxId, int progress, int numNewMessages);
870 
871         /**
872          * Callback for loadMessageForView
873          *
874          * @param result if null, the attachment completed - if non-null, terminating with failure
875          * @param messageId the message which contains the attachment
876          * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
877          */
loadMessageForViewCallback(MessagingException result, long messageId, int progress)878         public void loadMessageForViewCallback(MessagingException result, long messageId,
879                 int progress);
880 
881         /**
882          * Callback for loadAttachment
883          *
884          * @param result if null, the attachment completed - if non-null, terminating with failure
885          * @param messageId the message which contains the attachment
886          * @param attachmentId the attachment being loaded
887          * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
888          */
loadAttachmentCallback(MessagingException result, long messageId, long attachmentId, int progress)889         public void loadAttachmentCallback(MessagingException result, long messageId,
890                 long attachmentId, int progress);
891 
892         /**
893          * Callback for checkmail.  Note:  This looks a lot like updateMailboxCallback, but
894          * it's a separate call used only by the automatic checker service, so we can keep
895          * things separate.
896          *
897          * @param result If null, the operation completed without error
898          * @param accountId The account being operated on
899          * @param mailboxId The mailbox being operated on (may be unknown at start)
900          * @param progress 0 for "starting", no updates, 100 for complete
901          * @param tag the same tag that was passed to serviceCheckMail()
902          */
serviceCheckMailCallback(MessagingException result, long accountId, long mailboxId, int progress, long tag)903         public void serviceCheckMailCallback(MessagingException result, long accountId,
904                 long mailboxId, int progress, long tag);
905 
906         /**
907          * Callback for sending pending messages.  This will be called once to start the
908          * group, multiple times for messages, and once to complete the group.
909          *
910          * @param result If null, the operation completed without error
911          * @param accountId The account being operated on
912          * @param messageId The being sent (may be unknown at start)
913          * @param progress 0 for "starting", 100 for complete
914          */
sendMailCallback(MessagingException result, long accountId, long messageId, int progress)915         public void sendMailCallback(MessagingException result, long accountId,
916                 long messageId, int progress);
917     }
918 
919     /**
920      * Support for receiving callbacks from MessagingController and dealing with UI going
921      * out of scope.
922      */
923     private class LegacyListener extends MessagingListener {
924 
925         @Override
listFoldersStarted(long accountId)926         public void listFoldersStarted(long accountId) {
927             synchronized (mListeners) {
928                 for (Result l : mListeners) {
929                     l.updateMailboxListCallback(null, accountId, 0);
930                 }
931             }
932         }
933 
934         @Override
listFoldersFailed(long accountId, String message)935         public void listFoldersFailed(long accountId, String message) {
936             synchronized (mListeners) {
937                 for (Result l : mListeners) {
938                     l.updateMailboxListCallback(new MessagingException(message), accountId, 0);
939                 }
940             }
941         }
942 
943         @Override
listFoldersFinished(long accountId)944         public void listFoldersFinished(long accountId) {
945             synchronized (mListeners) {
946                 for (Result l : mListeners) {
947                     l.updateMailboxListCallback(null, accountId, 100);
948                 }
949             }
950         }
951 
952         @Override
synchronizeMailboxStarted(long accountId, long mailboxId)953         public void synchronizeMailboxStarted(long accountId, long mailboxId) {
954             synchronized (mListeners) {
955                 for (Result l : mListeners) {
956                     l.updateMailboxCallback(null, accountId, mailboxId, 0, 0);
957                 }
958             }
959         }
960 
961         @Override
synchronizeMailboxFinished(long accountId, long mailboxId, int totalMessagesInMailbox, int numNewMessages)962         public void synchronizeMailboxFinished(long accountId, long mailboxId,
963                 int totalMessagesInMailbox, int numNewMessages) {
964             synchronized (mListeners) {
965                 for (Result l : mListeners) {
966                     l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages);
967                 }
968             }
969         }
970 
971         @Override
synchronizeMailboxFailed(long accountId, long mailboxId, Exception e)972         public void synchronizeMailboxFailed(long accountId, long mailboxId, Exception e) {
973             MessagingException me;
974             if (e instanceof MessagingException) {
975                 me = (MessagingException) e;
976             } else {
977                 me = new MessagingException(e.toString());
978             }
979             synchronized (mListeners) {
980                 for (Result l : mListeners) {
981                     l.updateMailboxCallback(me, accountId, mailboxId, 0, 0);
982                 }
983             }
984         }
985 
986         @Override
checkMailStarted(Context context, long accountId, long tag)987         public void checkMailStarted(Context context, long accountId, long tag) {
988             synchronized (mListeners) {
989                 for (Result l : mListeners) {
990                     l.serviceCheckMailCallback(null, accountId, -1, 0, tag);
991                 }
992             }
993         }
994 
995         @Override
checkMailFinished(Context context, long accountId, long folderId, long tag)996         public void checkMailFinished(Context context, long accountId, long folderId, long tag) {
997             synchronized (mListeners) {
998                 for (Result l : mListeners) {
999                     l.serviceCheckMailCallback(null, accountId, folderId, 100, tag);
1000                 }
1001             }
1002         }
1003 
1004         @Override
loadMessageForViewStarted(long messageId)1005         public void loadMessageForViewStarted(long messageId) {
1006             synchronized (mListeners) {
1007                 for (Result listener : mListeners) {
1008                     listener.loadMessageForViewCallback(null, messageId, 0);
1009                 }
1010             }
1011         }
1012 
1013         @Override
loadMessageForViewFinished(long messageId)1014         public void loadMessageForViewFinished(long messageId) {
1015             synchronized (mListeners) {
1016                 for (Result listener : mListeners) {
1017                     listener.loadMessageForViewCallback(null, messageId, 100);
1018                 }
1019             }
1020         }
1021 
1022         @Override
loadMessageForViewFailed(long messageId, String message)1023         public void loadMessageForViewFailed(long messageId, String message) {
1024             synchronized (mListeners) {
1025                 for (Result listener : mListeners) {
1026                     listener.loadMessageForViewCallback(new MessagingException(message),
1027                             messageId, 0);
1028                 }
1029             }
1030         }
1031 
1032         @Override
loadAttachmentStarted(long accountId, long messageId, long attachmentId, boolean requiresDownload)1033         public void loadAttachmentStarted(long accountId, long messageId, long attachmentId,
1034                 boolean requiresDownload) {
1035             synchronized (mListeners) {
1036                 for (Result listener : mListeners) {
1037                     listener.loadAttachmentCallback(null, messageId, attachmentId, 0);
1038                 }
1039             }
1040         }
1041 
1042         @Override
loadAttachmentFinished(long accountId, long messageId, long attachmentId)1043         public void loadAttachmentFinished(long accountId, long messageId, long attachmentId) {
1044             synchronized (mListeners) {
1045                 for (Result listener : mListeners) {
1046                     listener.loadAttachmentCallback(null, messageId, attachmentId, 100);
1047                 }
1048             }
1049         }
1050 
1051         @Override
loadAttachmentFailed(long accountId, long messageId, long attachmentId, String reason)1052         public void loadAttachmentFailed(long accountId, long messageId, long attachmentId,
1053                 String reason) {
1054             synchronized (mListeners) {
1055                 for (Result listener : mListeners) {
1056                     listener.loadAttachmentCallback(new MessagingException(reason),
1057                             messageId, attachmentId, 0);
1058                 }
1059             }
1060         }
1061 
1062         @Override
sendPendingMessagesStarted(long accountId, long messageId)1063         synchronized public void sendPendingMessagesStarted(long accountId, long messageId) {
1064             synchronized (mListeners) {
1065                 for (Result listener : mListeners) {
1066                     listener.sendMailCallback(null, accountId, messageId, 0);
1067                 }
1068             }
1069         }
1070 
1071         @Override
sendPendingMessagesCompleted(long accountId)1072         synchronized public void sendPendingMessagesCompleted(long accountId) {
1073             synchronized (mListeners) {
1074                 for (Result listener : mListeners) {
1075                     listener.sendMailCallback(null, accountId, -1, 100);
1076                 }
1077             }
1078         }
1079 
1080         @Override
sendPendingMessagesFailed(long accountId, long messageId, Exception reason)1081         synchronized public void sendPendingMessagesFailed(long accountId, long messageId,
1082                 Exception reason) {
1083             MessagingException me;
1084             if (reason instanceof MessagingException) {
1085                 me = (MessagingException) reason;
1086             } else {
1087                 me = new MessagingException(reason.toString());
1088             }
1089             synchronized (mListeners) {
1090                 for (Result listener : mListeners) {
1091                     listener.sendMailCallback(me, accountId, messageId, 0);
1092                 }
1093             }
1094         }
1095     }
1096 
1097     /**
1098      * Service callback for service operations
1099      */
1100     private class ServiceCallback extends IEmailServiceCallback.Stub {
1101 
1102         private final static boolean DEBUG_FAIL_DOWNLOADS = false;       // do not check in "true"
1103 
loadAttachmentStatus(long messageId, long attachmentId, int statusCode, int progress)1104         public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
1105                 int progress) {
1106             MessagingException result = mapStatusToException(statusCode);
1107             switch (statusCode) {
1108                 case EmailServiceStatus.SUCCESS:
1109                     progress = 100;
1110                     break;
1111                 case EmailServiceStatus.IN_PROGRESS:
1112                     if (DEBUG_FAIL_DOWNLOADS && progress > 75) {
1113                         result = new MessagingException(
1114                                 String.valueOf(EmailServiceStatus.CONNECTION_ERROR));
1115                     }
1116                     // discard progress reports that look like sentinels
1117                     if (progress < 0 || progress >= 100) {
1118                         return;
1119                     }
1120                     break;
1121             }
1122             synchronized (mListeners) {
1123                 for (Result listener : mListeners) {
1124                     listener.loadAttachmentCallback(result, messageId, attachmentId, progress);
1125                 }
1126             }
1127         }
1128 
1129         /**
1130          * Note, this is an incomplete implementation of this callback, because we are
1131          * not getting things back from Service in quite the same way as from MessagingController.
1132          * However, this is sufficient for basic "progress=100" notification that message send
1133          * has just completed.
1134          */
sendMessageStatus(long messageId, String subject, int statusCode, int progress)1135         public void sendMessageStatus(long messageId, String subject, int statusCode,
1136                 int progress) {
1137 //            Log.d(Email.LOG_TAG, "sendMessageStatus: messageId=" + messageId
1138 //                    + " statusCode=" + statusCode + " progress=" + progress);
1139 //            Log.d(Email.LOG_TAG, "sendMessageStatus: subject=" + subject);
1140             long accountId = -1;        // This should be in the callback
1141             MessagingException result = mapStatusToException(statusCode);
1142             switch (statusCode) {
1143                 case EmailServiceStatus.SUCCESS:
1144                     progress = 100;
1145                     break;
1146                 case EmailServiceStatus.IN_PROGRESS:
1147                     // discard progress reports that look like sentinels
1148                     if (progress < 0 || progress >= 100) {
1149                         return;
1150                     }
1151                     break;
1152             }
1153 //            Log.d(Email.LOG_TAG, "result=" + result + " messageId=" + messageId
1154 //                    + " progress=" + progress);
1155             synchronized(mListeners) {
1156                 for (Result listener : mListeners) {
1157                     listener.sendMailCallback(result, accountId, messageId, progress);
1158                 }
1159             }
1160         }
1161 
syncMailboxListStatus(long accountId, int statusCode, int progress)1162         public void syncMailboxListStatus(long accountId, int statusCode, int progress) {
1163             MessagingException result = mapStatusToException(statusCode);
1164             switch (statusCode) {
1165                 case EmailServiceStatus.SUCCESS:
1166                     progress = 100;
1167                     break;
1168                 case EmailServiceStatus.IN_PROGRESS:
1169                     // discard progress reports that look like sentinels
1170                     if (progress < 0 || progress >= 100) {
1171                         return;
1172                     }
1173                     break;
1174             }
1175             synchronized(mListeners) {
1176                 for (Result listener : mListeners) {
1177                     listener.updateMailboxListCallback(result, accountId, progress);
1178                 }
1179             }
1180         }
1181 
syncMailboxStatus(long mailboxId, int statusCode, int progress)1182         public void syncMailboxStatus(long mailboxId, int statusCode, int progress) {
1183             MessagingException result = mapStatusToException(statusCode);
1184             switch (statusCode) {
1185                 case EmailServiceStatus.SUCCESS:
1186                     progress = 100;
1187                     break;
1188                 case EmailServiceStatus.IN_PROGRESS:
1189                     // discard progress reports that look like sentinels
1190                     if (progress < 0 || progress >= 100) {
1191                         return;
1192                     }
1193                     break;
1194             }
1195             // TODO where do we get "number of new messages" as well?
1196             // TODO should pass this back instead of looking it up here
1197             // TODO smaller projection
1198             Mailbox mbx = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
1199             // The mailbox could have disappeared if the server commanded it
1200             if (mbx == null) return;
1201             long accountId = mbx.mAccountKey;
1202             synchronized(mListeners) {
1203                 for (Result listener : mListeners) {
1204                     listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0);
1205                 }
1206             }
1207         }
1208 
mapStatusToException(int statusCode)1209         private MessagingException mapStatusToException(int statusCode) {
1210             switch (statusCode) {
1211                 case EmailServiceStatus.SUCCESS:
1212                 case EmailServiceStatus.IN_PROGRESS:
1213                     return null;
1214 
1215                 case EmailServiceStatus.LOGIN_FAILED:
1216                     return new AuthenticationFailedException("");
1217 
1218                 case EmailServiceStatus.CONNECTION_ERROR:
1219                     return new MessagingException(MessagingException.IOERROR);
1220 
1221                 case EmailServiceStatus.SECURITY_FAILURE:
1222                     return new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
1223 
1224                 case EmailServiceStatus.MESSAGE_NOT_FOUND:
1225                 case EmailServiceStatus.ATTACHMENT_NOT_FOUND:
1226                 case EmailServiceStatus.FOLDER_NOT_DELETED:
1227                 case EmailServiceStatus.FOLDER_NOT_RENAMED:
1228                 case EmailServiceStatus.FOLDER_NOT_CREATED:
1229                 case EmailServiceStatus.REMOTE_EXCEPTION:
1230                     // TODO: define exception code(s) & UI string(s) for server-side errors
1231                 default:
1232                     return new MessagingException(String.valueOf(statusCode));
1233             }
1234         }
1235     }
1236 }
1237