Controller.java revision c6893ddf0fc1a647ca13a2b3aac2c68ca345de37
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.Mailbox;
23
24import android.content.ContentResolver;
25import android.content.ContentUris;
26import android.content.ContentValues;
27import android.content.Context;
28import android.database.Cursor;
29import android.net.Uri;
30import android.util.Log;
31
32import java.util.HashSet;
33
34/**
35 * New central controller/dispatcher for Email activities that may require remote operations.
36 * Handles disambiguating between legacy MessagingController operations and newer provider/sync
37 * based code.
38 */
39public class Controller {
40
41    static Controller sInstance;
42    private Context mContext;
43    private Context mProviderContext;
44    private MessagingController mLegacyController;
45    private HashSet<Result> mListeners = new HashSet<Result>();
46
47    private static String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] {
48        EmailContent.RECORD_ID,
49        EmailContent.MessageColumns.ACCOUNT_KEY
50    };
51    private static int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1;
52
53    protected Controller(Context _context) {
54        mContext = _context;
55        mProviderContext = _context;
56        mLegacyController = MessagingController.getInstance(mContext);
57    }
58
59    /**
60     * Gets or creates the singleton instance of Controller.
61     * @param _context The context that will be used for all underlying system access
62     */
63    public synchronized static Controller getInstance(Context _context) {
64        if (sInstance == null) {
65            sInstance = new Controller(_context);
66        }
67        return sInstance;
68    }
69
70    /**
71     * For testing only:  Inject a different context for provider access.  This will be
72     * used internally for access the underlying provider (e.g. getContentResolver().query()).
73     * @param providerContext the provider context to be used by this instance
74     */
75    public void setProviderContext(Context providerContext) {
76        mProviderContext = providerContext;
77    }
78
79    /**
80     * Any UI code that wishes for callback results (on async ops) should register their callback
81     * here (typically from onResume()).  Unregistered callbacks will never be called, to prevent
82     * problems when the command completes and the activity has already paused or finished.
83     * @param listener The callback that may be used in action methods
84     */
85    public void addResultCallback(Result listener) {
86        synchronized (mListeners) {
87            mListeners.add(listener);
88        }
89    }
90
91    /**
92     * Any UI code that no longer wishes for callback results (on async ops) should unregister
93     * their callback here (typically from onPause()).  Unregistered callbacks will never be called,
94     * to prevent problems when the command completes and the activity has already paused or
95     * finished.
96     * @param listener The callback that may no longer be used
97     */
98    public void removeResultCallback(Result listener) {
99        synchronized (mListeners) {
100            mListeners.remove(listener);
101        }
102    }
103
104    private boolean isActiveResultCallback(Result listener) {
105        synchronized (mListeners) {
106            return mListeners.contains(listener);
107        }
108    }
109
110    /**
111     * Request a remote update of mailboxes for an account.
112     *
113     * TODO: Implement (if any) for non-MessagingController
114     * TODO: Probably the right way is to create a fake "service" for MessagingController ops
115     */
116    public void updateMailboxList(final EmailContent.Account account, final Result callback) {
117
118        // 1. determine if we can use MessagingController for this
119        boolean legacyController = isMessagingController(account);
120
121        // 2. if not...?
122        // TODO: for now, just pretend "it worked"
123        if (!legacyController) {
124            if (callback != null) {
125                callback.updateMailboxListCallback(null, account.mId);
126            }
127            return;
128        }
129
130        // 3. if so, make the call
131        new Thread() {
132            @Override
133            public void run() {
134                MessagingListener listener = new LegacyListener(callback);
135                mLegacyController.addListener(listener);
136                mLegacyController.listFolders(account, listener);
137            }
138        }.start();
139    }
140
141    /**
142     * Request a remote update of a mailbox.
143     *
144     * The contract here should be to try and update the headers ASAP, in order to populate
145     * a simple message list.  We should also at this point queue up a background task of
146     * downloading some/all of the messages in this mailbox, but that should be interruptable.
147     */
148    public void updateMailbox(final EmailContent.Account account,
149            final EmailContent.Mailbox mailbox, final Result callback) {
150
151        // 1. determine if we can use MessagingController for this
152        boolean legacyController = isMessagingController(account);
153
154        // 2. if not...?
155        // TODO: for now, just pretend "it worked"
156        if (!legacyController) {
157            if (callback != null) {
158                callback.updateMailboxCallback(null, account.mId, mailbox.mId, -1, -1);
159            }
160            return;
161        }
162
163        // 3. if so, make the call
164        new Thread() {
165            @Override
166            public void run() {
167                MessagingListener listener = new LegacyListener(callback);
168                mLegacyController.addListener(listener);
169                mLegacyController.synchronizeMailbox(account, mailbox, listener);
170            }
171        }.start();
172    }
173
174    /**
175     * Saves the message to a mailbox of given type.
176     * @param message the message (must have the mAccountId set).
177     * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS).
178     * TODO: UI feedback.
179     * TODO: use AsyncTask instead of Thread
180     */
181    public void saveToMailbox(final EmailContent.Message message, final int mailboxType) {
182        new Thread() {
183            @Override
184            public void run() {
185                long accountId = message.mAccountKey;
186                long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType);
187                message.mMailboxKey = mailboxId;
188                message.save(mContext);
189            }
190        }.start();
191    }
192
193    /**
194     * @param accountId the account id
195     * @param mailboxType the mailbox type (e.g.  EmailContent.Mailbox.TYPE_TRASH)
196     * @return the id of the mailbox. The mailbox is created if not existing.
197     * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative.
198     * Does not validate the input in other ways (e.g. does not verify the existence of account).
199     */
200    public long findOrCreateMailboxOfType(long accountId, int mailboxType) {
201        if (accountId < 0 || mailboxType < 0) {
202            return Mailbox.NO_MAILBOX;
203        }
204        long mailboxId =
205            Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType);
206        return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId;
207    }
208
209    /**
210     * @param mailboxType the mailbox type
211     * @return the resource string corresponding to the mailbox type, empty if not found.
212     */
213    /* package */ String getSpecialMailboxDisplayName(int mailboxType) {
214        int resId = -1;
215        switch (mailboxType) {
216        case Mailbox.TYPE_INBOX:
217            // TODO: there is no special_mailbox_display_name_inbox; why?
218            resId = R.string.special_mailbox_name_inbox;
219            break;
220        case Mailbox.TYPE_OUTBOX:
221            resId = R.string.special_mailbox_display_name_outbox;
222            break;
223        case Mailbox.TYPE_DRAFTS:
224            resId = R.string.special_mailbox_display_name_drafts;
225            break;
226        case Mailbox.TYPE_TRASH:
227            resId = R.string.special_mailbox_display_name_trash;
228            break;
229        case Mailbox.TYPE_SENT:
230            resId = R.string.special_mailbox_display_name_sent;
231            break;
232        }
233        return resId != -1 ? mContext.getString(resId) : "";
234    }
235
236    /**
237     * Create a mailbox given the account and mailboxType.
238     * TODO: Does this need to be signaled explicitly to the sync engines?
239     * As this method is only used internally ('private'), it does not
240     * validate its inputs (accountId and mailboxType).
241     */
242    /* package */ long createMailbox(long accountId, int mailboxType) {
243        if (accountId < 0 || mailboxType < 0) {
244            String mes = "Invalid arguments " + accountId + ' ' + mailboxType;
245            Log.e(Email.LOG_TAG, mes);
246            throw new RuntimeException(mes);
247        }
248        Mailbox box = new Mailbox();
249        box.mAccountKey = accountId;
250        box.mType = mailboxType;
251        box.mSyncFrequency = EmailContent.Account.CHECK_INTERVAL_NEVER;
252        box.mFlagVisible = true;
253        box.mDisplayName = getSpecialMailboxDisplayName(mailboxType);
254        box.saveOrUpdate(mProviderContext);
255        return box.mId;
256    }
257
258    /**
259     * Delete a single message by moving it to the trash.
260     *
261     * This function has no callback, no result reporting, because the desired outcome
262     * is reflected entirely by changes to one or more cursors.
263     *
264     * @param messageId The id of the message to "delete".
265     * @param accountId The id of the message's account, or -1 if not known by caller
266     *
267     * TODO: Move out of UI thread
268     * TODO: "get account a for message m" should be a utility
269     * TODO: "get mailbox of type n for account a" should be a utility
270     */
271     public void deleteMessage(long messageId, long accountId) {
272        ContentResolver resolver = mProviderContext.getContentResolver();
273
274        // 1.  Look up acct# for message we're deleting
275        Cursor c = null;
276        if (accountId == -1) {
277            try {
278                c = resolver.query(EmailContent.Message.CONTENT_URI,
279                        MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?",
280                        new String[] { Long.toString(messageId) }, null);
281                if (c.moveToFirst()) {
282                    accountId = c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID);
283                } else {
284                    return;
285                }
286            } finally {
287                if (c != null) c.close();
288            }
289        }
290
291        // 2. Confirm that there is a trash mailbox available
292        // 3.  If there's no trash mailbox, create one
293        // TODO: Does this need to be signaled explicitly to the sync engines?
294        long trashMailboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_TRASH);
295
296        // 4.  Change the mailbox key for the message we're "deleting"
297        ContentValues cv = new ContentValues();
298        cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId);
299        Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
300        resolver.update(uri, cv, null, null);
301
302        // 5.  Drop non-essential data for the message (e.g. attachments)
303        // TODO: find the actual files (if any, if loaded) & delete them
304        c = null;
305        try {
306            c = resolver.query(EmailContent.Attachment.CONTENT_URI,
307                    EmailContent.Attachment.CONTENT_PROJECTION,
308                    EmailContent.AttachmentColumns.MESSAGE_KEY + "=?",
309                    new String[] { Long.toString(messageId) }, null);
310            while (c.moveToNext()) {
311                // delete any associated storage
312                // delete row?
313            }
314        } finally {
315            if (c != null) c.close();
316        }
317
318        // 6.  For IMAP/POP3 we may need to kick off an immediate delete (depends on acct settings)
319        // TODO write this
320    }
321
322    /**
323     * Simple helper to determine if legacy MessagingController should be used
324     */
325    private boolean isMessagingController(EmailContent.Account account) {
326        Store.StoreInfo info =
327            Store.StoreInfo.getStoreInfo(account.getStoreUri(mContext), mContext);
328        String scheme = info.mScheme;
329
330        return ("pop3".equals(scheme) || "imap".equals(scheme));
331    }
332
333    /**
334     * Simple callback for synchronous commands.  For many commands, this can be largely ignored
335     * and the result is observed via provider cursors.  The callback will *not* necessarily be
336     * made from the UI thread, so you may need further handlers to safely make UI updates.
337     */
338    public interface Result {
339
340        /**
341         * Callback for updateMailboxList
342         *
343         * @param result If null, the operation completed without error
344         * @param accountKey The account being operated on
345         */
346        public void updateMailboxListCallback(MessagingException result, long accountKey);
347
348        /**
349         * Callback for updateMailbox
350         *
351         * @param result If null, the operation completed without error
352         * @param accountKey The account being operated on
353         * @param mailboxKey The mailbox being operated on
354         */
355        public void updateMailboxCallback(MessagingException result, long accountKey,
356                long mailboxKey, int totalMessagesInMailbox, int numNewMessages);
357    }
358
359    /**
360     * Support for receiving callbacks from MessagingController and dealing with UI going
361     * out of scope.
362     */
363    private class LegacyListener extends MessagingListener {
364        Result mResultCallback;
365
366        public LegacyListener(Result callback) {
367            mResultCallback = callback;
368        }
369
370        @Override
371        public void listFoldersFailed(EmailContent.Account account, String message) {
372            if (mResultCallback != null && isActiveResultCallback(mResultCallback)) {
373                mResultCallback.updateMailboxListCallback(new MessagingException(message),
374                        account.mId);
375            }
376            mLegacyController.removeListener(this);
377        }
378
379        @Override
380        public void listFoldersFinished(EmailContent.Account account) {
381            if (mResultCallback != null && isActiveResultCallback(mResultCallback)) {
382                mResultCallback.updateMailboxListCallback(null, account.mId);
383            }
384            mLegacyController.removeListener(this);
385        }
386
387        @Override
388        public void synchronizeMailboxFinished(EmailContent.Account account,
389                EmailContent.Mailbox folder, int totalMessagesInMailbox, int numNewMessages) {
390            if (mResultCallback != null && isActiveResultCallback(mResultCallback)) {
391                mResultCallback.updateMailboxCallback(null, account.mId, folder.mId,
392                        totalMessagesInMailbox, numNewMessages);
393            }
394            mLegacyController.removeListener(this);
395        }
396
397        @Override
398        public void synchronizeMailboxFailed(EmailContent.Account account,
399                EmailContent.Mailbox folder, Exception e) {
400            if (mResultCallback != null && isActiveResultCallback(mResultCallback)) {
401                MessagingException me;
402                if (e instanceof MessagingException) {
403                    me = (MessagingException) e;
404                } else {
405                    me = new MessagingException(e.toString());
406                }
407                mResultCallback.updateMailboxCallback(me, account.mId, folder.mId, -1, -1);
408            }
409            mLegacyController.removeListener(this);
410        }
411
412
413    }
414
415
416}
417