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