Controller.java revision b8a781f220617d6e7750c5e9f093742206add45f
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
17package com.android.email;
18
19import com.android.email.mail.MessagingException;
20import com.android.email.mail.Store;
21import com.android.email.provider.EmailContent;
22import com.android.email.provider.EmailContent.Account;
23import com.android.email.provider.EmailContent.Attachment;
24import com.android.email.provider.EmailContent.Mailbox;
25import com.android.email.provider.EmailContent.Message;
26import com.android.email.service.EmailServiceProxy;
27import com.android.exchange.EmailServiceStatus;
28import com.android.exchange.IEmailService;
29import com.android.exchange.IEmailServiceCallback;
30import com.android.exchange.SyncManager;
31
32import android.content.ContentResolver;
33import android.content.ContentUris;
34import android.content.ContentValues;
35import android.content.Context;
36import android.database.Cursor;
37import android.net.Uri;
38import android.os.RemoteException;
39import android.util.Log;
40
41import java.util.HashSet;
42
43/**
44 * New central controller/dispatcher for Email activities that may require remote operations.
45 * Handles disambiguating between legacy MessagingController operations and newer provider/sync
46 * based code.
47 */
48public class Controller {
49
50    static Controller sInstance;
51    private Context mContext;
52    private Context mProviderContext;
53    private MessagingController mLegacyController;
54    private HashSet<Result> mListeners = new HashSet<Result>();
55
56    private static String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] {
57        EmailContent.RECORD_ID,
58        EmailContent.MessageColumns.ACCOUNT_KEY
59    };
60    private static int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1;
61
62    protected Controller(Context _context) {
63        mContext = _context;
64        mProviderContext = _context;
65        mLegacyController = MessagingController.getInstance(mContext);
66    }
67
68    /**
69     * Gets or creates the singleton instance of Controller.
70     * @param _context The context that will be used for all underlying system access
71     */
72    public synchronized static Controller getInstance(Context _context) {
73        if (sInstance == null) {
74            sInstance = new Controller(_context);
75        }
76        return sInstance;
77    }
78
79    /**
80     * For testing only:  Inject a different context for provider access.  This will be
81     * used internally for access the underlying provider (e.g. getContentResolver().query()).
82     * @param providerContext the provider context to be used by this instance
83     */
84    public void setProviderContext(Context providerContext) {
85        mProviderContext = providerContext;
86    }
87
88    /**
89     * Any UI code that wishes for callback results (on async ops) should register their callback
90     * here (typically from onResume()).  Unregistered callbacks will never be called, to prevent
91     * problems when the command completes and the activity has already paused or finished.
92     * @param listener The callback that may be used in action methods
93     */
94    public void addResultCallback(Result listener) {
95        synchronized (mListeners) {
96            mListeners.add(listener);
97        }
98    }
99
100    /**
101     * Any UI code that no longer wishes for callback results (on async ops) should unregister
102     * their callback here (typically from onPause()).  Unregistered callbacks will never be called,
103     * to prevent problems when the command completes and the activity has already paused or
104     * finished.
105     * @param listener The callback that may no longer be used
106     */
107    public void removeResultCallback(Result listener) {
108        synchronized (mListeners) {
109            mListeners.remove(listener);
110        }
111    }
112
113    private boolean isActiveResultCallback(Result listener) {
114        synchronized (mListeners) {
115            return mListeners.contains(listener);
116        }
117    }
118
119    /**
120     * Request a remote update of mailboxes for an account.
121     *
122     * TODO: Implement (if any) for non-MessagingController
123     * TODO: Probably the right way is to create a fake "service" for MessagingController ops
124     */
125    public void updateMailboxList(final EmailContent.Account account, final Result callback) {
126
127        // 1. determine if we can use MessagingController for this
128        boolean legacyController = isMessagingController(account);
129
130        // 2. if not...?
131        // TODO: for now, just pretend "it worked"
132        if (!legacyController) {
133            if (callback != null) {
134                callback.updateMailboxListCallback(null, account.mId);
135            }
136            return;
137        }
138
139        // 3. if so, make the call
140        new Thread() {
141            @Override
142            public void run() {
143                MessagingListener listener = new LegacyListener(callback);
144                mLegacyController.addListener(listener);
145                mLegacyController.listFolders(account, listener);
146            }
147        }.start();
148    }
149
150    /**
151     * Request a remote update of a mailbox.
152     *
153     * The contract here should be to try and update the headers ASAP, in order to populate
154     * a simple message list.  We should also at this point queue up a background task of
155     * downloading some/all of the messages in this mailbox, but that should be interruptable.
156     */
157    public void updateMailbox(final EmailContent.Account account,
158            final EmailContent.Mailbox mailbox, final Result callback) {
159
160        // 1. determine if we can use MessagingController for this
161        boolean legacyController = isMessagingController(account);
162
163        // 2. if not...?
164        // TODO: for now, just pretend "it worked"
165        if (!legacyController) {
166            if (callback != null) {
167                callback.updateMailboxCallback(null, account.mId, mailbox.mId, -1, -1);
168            }
169            return;
170        }
171
172        // 3. if so, make the call
173        new Thread() {
174            @Override
175            public void run() {
176                MessagingListener listener = new LegacyListener(callback);
177                mLegacyController.addListener(listener);
178                mLegacyController.synchronizeMailbox(account, mailbox, listener);
179            }
180        }.start();
181    }
182
183    /**
184     * Saves the message to a mailbox of given type.
185     * @param message the message (must have the mAccountId set).
186     * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS).
187     * TODO: UI feedback.
188     * TODO: use AsyncTask instead of Thread
189     */
190    public void saveToMailbox(final EmailContent.Message message, final int mailboxType) {
191        new Thread() {
192            @Override
193            public void run() {
194                long accountId = message.mAccountKey;
195                long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType);
196                message.mMailboxKey = mailboxId;
197                message.save(mContext);
198            }
199        }.start();
200    }
201
202    /**
203     * @param accountId the account id
204     * @param mailboxType the mailbox type (e.g.  EmailContent.Mailbox.TYPE_TRASH)
205     * @return the id of the mailbox. The mailbox is created if not existing.
206     * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative.
207     * Does not validate the input in other ways (e.g. does not verify the existence of account).
208     */
209    public long findOrCreateMailboxOfType(long accountId, int mailboxType) {
210        if (accountId < 0 || mailboxType < 0) {
211            return Mailbox.NO_MAILBOX;
212        }
213        long mailboxId =
214            Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType);
215        return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId;
216    }
217
218    /**
219     * @param mailboxType the mailbox type
220     * @return the resource string corresponding to the mailbox type, empty if not found.
221     */
222    /* package */ String getSpecialMailboxDisplayName(int mailboxType) {
223        int resId = -1;
224        switch (mailboxType) {
225            case Mailbox.TYPE_INBOX:
226                // TODO: there is no special_mailbox_display_name_inbox; why?
227                resId = R.string.special_mailbox_name_inbox;
228                break;
229            case Mailbox.TYPE_OUTBOX:
230                resId = R.string.special_mailbox_display_name_outbox;
231                break;
232            case Mailbox.TYPE_DRAFTS:
233                resId = R.string.special_mailbox_display_name_drafts;
234                break;
235            case Mailbox.TYPE_TRASH:
236                resId = R.string.special_mailbox_display_name_trash;
237                break;
238            case Mailbox.TYPE_SENT:
239                resId = R.string.special_mailbox_display_name_sent;
240                break;
241        }
242        return resId != -1 ? mContext.getString(resId) : "";
243    }
244
245    /**
246     * Create a mailbox given the account and mailboxType.
247     * TODO: Does this need to be signaled explicitly to the sync engines?
248     * As this method is only used internally ('private'), it does not
249     * validate its inputs (accountId and mailboxType).
250     */
251    /* package */ long createMailbox(long accountId, int mailboxType) {
252        if (accountId < 0 || mailboxType < 0) {
253            String mes = "Invalid arguments " + accountId + ' ' + mailboxType;
254            Log.e(Email.LOG_TAG, mes);
255            throw new RuntimeException(mes);
256        }
257        Mailbox box = new Mailbox();
258        box.mAccountKey = accountId;
259        box.mType = mailboxType;
260        box.mSyncInterval = EmailContent.Account.CHECK_INTERVAL_NEVER;
261        box.mFlagVisible = true;
262        box.mDisplayName = getSpecialMailboxDisplayName(mailboxType);
263        box.save(mProviderContext);
264        return box.mId;
265    }
266
267    /**
268     * Delete a single message by moving it to the trash.
269     *
270     * This function has no callback, no result reporting, because the desired outcome
271     * is reflected entirely by changes to one or more cursors.
272     *
273     * @param messageId The id of the message to "delete".
274     * @param accountId The id of the message's account, or -1 if not known by caller
275     *
276     * TODO: Move out of UI thread
277     * TODO: "get account a for message m" should be a utility
278     * TODO: "get mailbox of type n for account a" should be a utility
279     */
280    public void deleteMessage(long messageId, long accountId) {
281        ContentResolver resolver = mProviderContext.getContentResolver();
282
283        // 1.  Look up acct# for message we're deleting
284        Cursor c = null;
285        if (accountId == -1) {
286            try {
287                c = resolver.query(EmailContent.Message.CONTENT_URI,
288                        MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?",
289                        new String[] { Long.toString(messageId) }, null);
290                if (c.moveToFirst()) {
291                    accountId = c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID);
292                } else {
293                    return;
294                }
295            } finally {
296                if (c != null) c.close();
297            }
298        }
299
300        // 2. Confirm that there is a trash mailbox available
301        // 3.  If there's no trash mailbox, create one
302        // TODO: Does this need to be signaled explicitly to the sync engines?
303        long trashMailboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_TRASH);
304
305        // 4.  Change the mailbox key for the message we're "deleting"
306        ContentValues cv = new ContentValues();
307        cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId);
308        Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
309        resolver.update(uri, cv, null, null);
310
311        // 5.  Drop non-essential data for the message (e.g. attachments)
312        // TODO: find the actual files (if any, if loaded) & delete them
313        c = null;
314        try {
315            c = resolver.query(EmailContent.Attachment.CONTENT_URI,
316                    EmailContent.Attachment.CONTENT_PROJECTION,
317                    EmailContent.AttachmentColumns.MESSAGE_KEY + "=?",
318                    new String[] { Long.toString(messageId) }, null);
319            while (c.moveToNext()) {
320                // delete any associated storage
321                // delete row?
322            }
323        } finally {
324            if (c != null) c.close();
325        }
326
327        // 6.  For IMAP/POP3 we may need to kick off an immediate delete (depends on acct settings)
328        // TODO write this
329    }
330
331    /**
332     * Set/clear the unread status of a message
333     *
334     * @param messageId the message to update
335     * @param isRead the new value for the isRead flag
336     */
337    public void setMessageRead(long messageId, boolean isRead) {
338        // TODO this should not be in this thread. queue it up.
339        // TODO Also, it needs to update the read/unread count in the mailbox
340        // TODO kick off service/messagingcontroller actions
341
342        ContentValues cv = new ContentValues();
343        cv.put(EmailContent.MessageColumns.FLAG_READ, isRead);
344        Uri uri = ContentUris.withAppendedId(
345                EmailContent.Message.SYNCED_CONTENT_URI, messageId);
346        mProviderContext.getContentResolver().update(uri, cv, null, null);
347    }
348
349    /**
350     * Set/clear the favorite status of a message
351     *
352     * @param messageId the message to update
353     * @param isFavorite the new value for the isFavorite flag
354     */
355    public void setMessageFavorite(long messageId, boolean isFavorite) {
356        // TODO this should not be in this thread. queue it up.
357        // TODO kick off service/messagingcontroller actions
358
359        ContentValues cv = new ContentValues();
360        cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite);
361        Uri uri = ContentUris.withAppendedId(
362                EmailContent.Message.SYNCED_CONTENT_URI, messageId);
363        mProviderContext.getContentResolver().update(uri, cv, null, null);
364    }
365
366    /**
367     * Request that an attachment be loaded
368     *
369     * @param save If true, attachment will be saved into a well-known place e.g. sdcard
370     * @param attachmentId the attachment to load
371     * @param messageId the owner message
372     * @param callback the Controller callback by which results will be reported
373     */
374    public void loadAttachment(boolean save, long attachmentId, long messageId,
375            final Result callback, Object tag) {
376
377        Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId);
378
379        // Split here for target type (Service or MessagingController)
380        IEmailService service = getServiceForMessage(messageId);
381        if (service != null) {
382            // Service implementation
383            try {
384                service.loadAttachment(attachInfo.mId, null,
385                        new LoadAttachmentCallback(callback, tag));
386            } catch (RemoteException e) {
387                // TODO Change exception handling to be consistent with however this method
388                // is implemented for other protocols
389                Log.e("onDownloadAttachment", "RemoteException", e);
390            }
391        } else {
392            // MessagingController implementation
393        }
394    }
395
396    /**
397     * For a given message id, return a service proxy if applicable, or null.
398     *
399     * @param messageId the message of interest
400     * @result service proxy, or null if n/a
401     */
402    private IEmailService getServiceForMessage(long messageId) {
403        // TODO make this more efficient, caching the account, smaller lookup here, etc.
404        Message message = Message.restoreMessageWithId(mProviderContext, messageId);
405        long accountId = message.mAccountKey;
406        Account account = EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
407        if (isMessagingController(account)) {
408            return null;
409        } else {
410            return new EmailServiceProxy(mContext, SyncManager.class);
411        }
412    }
413
414    /**
415     * Simple helper to determine if legacy MessagingController should be used
416     *
417     * TODO this should not require a full account, just an accountId
418     * TODO this should use a cache because we'll be doing this a lot
419     */
420    private boolean isMessagingController(EmailContent.Account account) {
421        Store.StoreInfo info =
422            Store.StoreInfo.getStoreInfo(account.getStoreUri(mContext), mContext);
423        String scheme = info.mScheme;
424
425        return ("pop3".equals(scheme) || "imap".equals(scheme));
426    }
427
428    /**
429     * Simple callback for synchronous commands.  For many commands, this can be largely ignored
430     * and the result is observed via provider cursors.  The callback will *not* necessarily be
431     * made from the UI thread, so you may need further handlers to safely make UI updates.
432     */
433    public interface Result {
434
435        /**
436         * Callback for updateMailboxList
437         *
438         * @param result If null, the operation completed without error
439         * @param accountId The account being operated on
440         */
441        public void updateMailboxListCallback(MessagingException result, long accountId);
442
443        /**
444         * Callback for updateMailbox
445         *
446         * @param result If null, the operation completed without error
447         * @param accountId The account being operated on
448         * @param mailboxId The mailbox being operated on
449         */
450        public void updateMailboxCallback(MessagingException result, long accountId,
451                long mailboxId, int totalMessagesInMailbox, int numNewMessages);
452
453        /**
454         * Callback for loadAttachment
455         *
456         * @param result if null, the attachment completed - if non-null, terminating with failure
457         * @param messageId the message which contains the attachment
458         * @param attachmentId the attachment being loaded
459         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
460         * @param tag caller-defined tag, if supplied
461         */
462        public void loadAttachmentCallback(MessagingException result, long messageId,
463                long attachmentId, int progress, Object tag);
464    }
465
466    /**
467     * Support for receiving callbacks from MessagingController and dealing with UI going
468     * out of scope.
469     */
470    private class LegacyListener extends MessagingListener {
471        Result mResultCallback;
472
473        public LegacyListener(Result callback) {
474            mResultCallback = callback;
475        }
476
477        @Override
478        public void listFoldersFailed(EmailContent.Account account, String message) {
479            if (mResultCallback != null && isActiveResultCallback(mResultCallback)) {
480                mResultCallback.updateMailboxListCallback(new MessagingException(message),
481                        account.mId);
482            }
483            mLegacyController.removeListener(this);
484        }
485
486        @Override
487        public void listFoldersFinished(EmailContent.Account account) {
488            if (mResultCallback != null && isActiveResultCallback(mResultCallback)) {
489                mResultCallback.updateMailboxListCallback(null, account.mId);
490            }
491            mLegacyController.removeListener(this);
492        }
493
494        @Override
495        public void synchronizeMailboxFinished(EmailContent.Account account,
496                EmailContent.Mailbox folder, int totalMessagesInMailbox, int numNewMessages) {
497            if (mResultCallback != null && isActiveResultCallback(mResultCallback)) {
498                mResultCallback.updateMailboxCallback(null, account.mId, folder.mId,
499                        totalMessagesInMailbox, numNewMessages);
500            }
501            mLegacyController.removeListener(this);
502        }
503
504        @Override
505        public void synchronizeMailboxFailed(EmailContent.Account account,
506                EmailContent.Mailbox folder, Exception e) {
507            if (mResultCallback != null && isActiveResultCallback(mResultCallback)) {
508                MessagingException me;
509                if (e instanceof MessagingException) {
510                    me = (MessagingException) e;
511                } else {
512                    me = new MessagingException(e.toString());
513                }
514                mResultCallback.updateMailboxCallback(me, account.mId, folder.mId, -1, -1);
515            }
516            mLegacyController.removeListener(this);
517        }
518
519
520    }
521
522    /**
523     * Service callback for load attachment
524     */
525    private class LoadAttachmentCallback extends IEmailServiceCallback.Stub {
526
527        private final static boolean DEBUG_FAIL_DOWNLOADS = false;       // do not check in "true"
528
529        Result mCallback;
530        boolean mMadeFirstCallback;
531        Object mTag;
532
533        public LoadAttachmentCallback(Result callback, Object tag) {
534            super();
535            mCallback = callback;
536            mMadeFirstCallback = false;
537            mTag = tag;
538        }
539
540        /**
541         * Callback from Service for load attachment status.
542         *
543         * This performs some translations to what the UI expects, which is (assuming no fail):
544         *  progress = 0 ("started")
545         *  progress = 1..99 ("running")
546         *  progress = 100 ("finished")
547         *
548         * @param messageId the id of the message the callback relates to
549         * @param attachmentId the id of the attachment (if any)
550         * @param statusCode from the definitions in EmailServiceStatus
551         * @param progress the progress (from 0 to 100) of a download
552         */
553        public void status(long messageId, long attachmentId, int statusCode, int progress) {
554            if (mCallback != null && isActiveResultCallback(mCallback)) {
555                MessagingException result = null;
556                switch (statusCode) {
557                    case EmailServiceStatus.SUCCESS:
558                        progress = 100;
559                        break;
560                    case EmailServiceStatus.IN_PROGRESS:
561                        // special case, force a single "progress = 0" for the first time
562                        if (!mMadeFirstCallback) {
563                            progress = 0;
564                            mMadeFirstCallback = true;
565                        } else if (DEBUG_FAIL_DOWNLOADS && progress > 75) {
566                            result = new MessagingException(
567                                    String.valueOf(EmailServiceStatus.CONNECTION_ERROR));
568                        } else if (progress <= 0 || progress >= 100) {
569                            return;
570                        }
571                        break;
572                    default:
573                        result = new MessagingException(String.valueOf(statusCode));
574                    break;
575                }
576                mCallback.loadAttachmentCallback(result, messageId, attachmentId, progress, mTag);
577                // prevent any trailing reports if there was an error
578                if (result != null) {
579                    mCallback = null;
580                }
581            }
582        }
583    }
584}
585