Pop3Service.java revision 7f7f7e6402eec1baab6bedcb58da61369cae4097
1/*
2 * Copyright (C) 2012 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.service;
18
19import android.app.Service;
20import android.content.ContentResolver;
21import android.content.ContentUris;
22import android.content.ContentValues;
23import android.content.Context;
24import android.content.Intent;
25import android.database.Cursor;
26import android.net.TrafficStats;
27import android.net.Uri;
28import android.os.IBinder;
29import android.os.RemoteCallbackList;
30import android.os.RemoteException;
31import android.util.Log;
32
33import com.android.email.NotificationController;
34import com.android.email.mail.Store;
35import com.android.email.mail.store.Pop3Store;
36import com.android.email.mail.store.Pop3Store.Pop3Folder;
37import com.android.email.mail.store.Pop3Store.Pop3Message;
38import com.android.email.provider.Utilities;
39import com.android.email2.ui.MailActivityEmail;
40import com.android.emailcommon.Logging;
41import com.android.emailcommon.TrafficFlags;
42import com.android.emailcommon.mail.AuthenticationFailedException;
43import com.android.emailcommon.mail.Folder.OpenMode;
44import com.android.emailcommon.mail.MessagingException;
45import com.android.emailcommon.provider.Account;
46import com.android.emailcommon.provider.EmailContent;
47import com.android.emailcommon.provider.EmailContent.Attachment;
48import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
49import com.android.emailcommon.provider.EmailContent.MailboxColumns;
50import com.android.emailcommon.provider.EmailContent.Message;
51import com.android.emailcommon.provider.EmailContent.MessageColumns;
52import com.android.emailcommon.provider.EmailContent.SyncColumns;
53import com.android.emailcommon.provider.Mailbox;
54import com.android.emailcommon.service.EmailServiceCallback;
55import com.android.emailcommon.service.EmailServiceStatus;
56import com.android.emailcommon.service.IEmailServiceCallback;
57import com.android.emailcommon.utility.AttachmentUtilities;
58import com.android.mail.providers.UIProvider;
59import com.android.mail.providers.UIProvider.AccountCapabilities;
60import com.android.mail.providers.UIProvider.AttachmentState;
61
62import java.io.IOException;
63import java.util.ArrayList;
64import java.util.HashMap;
65import java.util.HashSet;
66
67public class Pop3Service extends Service {
68    private static final String TAG = "Pop3Service";
69
70    @Override
71    public int onStartCommand(Intent intent, int flags, int startId) {
72        return Service.START_STICKY;
73    }
74
75    // Callbacks as set up via setCallback
76    private static final RemoteCallbackList<IEmailServiceCallback> mCallbackList =
77            new RemoteCallbackList<IEmailServiceCallback>();
78
79    private static final EmailServiceCallback sCallbackProxy =
80            new EmailServiceCallback(mCallbackList);
81
82    /**
83     * Create our EmailService implementation here.
84     */
85    private final EmailServiceStub mBinder = new EmailServiceStub() {
86
87        @Override
88        public void setCallback(IEmailServiceCallback cb) throws RemoteException {
89            mCallbackList.register(cb);
90        }
91
92        @Override
93        public int getCapabilities(Account acct) throws RemoteException {
94            return AccountCapabilities.UNDO;
95        }
96
97        @Override
98        public void loadAttachment(long attachmentId, boolean background) throws RemoteException {
99            Attachment att = Attachment.restoreAttachmentWithId(mContext, attachmentId);
100            if (att == null || att.mUiState != AttachmentState.DOWNLOADING) return;
101            long inboxId = Mailbox.findMailboxOfType(mContext, att.mAccountKey, Mailbox.TYPE_INBOX);
102            if (inboxId == Mailbox.NO_MAILBOX) return;
103            // We load attachments during a sync
104            startSync(inboxId, true);
105        }
106    };
107
108    @Override
109    public IBinder onBind(Intent intent) {
110        mBinder.init(this, sCallbackProxy);
111        return mBinder;
112    }
113
114    private static void sendMailboxStatus(Mailbox mailbox, int status) {
115            sCallbackProxy.syncMailboxStatus(mailbox.mId, status, 0);
116    }
117
118    /**
119     * Start foreground synchronization of the specified folder. This is called
120     * by synchronizeMailbox or checkMail. TODO this should use ID's instead of
121     * fully-restored objects
122     *
123     * @param account
124     * @param folder
125     * @throws MessagingException
126     */
127    public static void synchronizeMailboxSynchronous(Context context, final Account account,
128            final Mailbox folder) throws MessagingException {
129        sendMailboxStatus(folder, EmailServiceStatus.IN_PROGRESS);
130
131        TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
132        if ((folder.mFlags & Mailbox.FLAG_HOLDS_MAIL) == 0) {
133            sendMailboxStatus(folder, EmailServiceStatus.SUCCESS);
134        }
135        NotificationController nc = NotificationController.getInstance(context);
136        try {
137            synchronizePop3Mailbox(context, account, folder);
138            // Clear authentication notification for this account
139            nc.cancelLoginFailedNotification(account.mId);
140            sendMailboxStatus(folder, EmailServiceStatus.SUCCESS);
141        } catch (MessagingException e) {
142            if (Logging.LOGD) {
143                Log.v(Logging.LOG_TAG, "synchronizeMailbox", e);
144            }
145            if (e instanceof AuthenticationFailedException) {
146                // Generate authentication notification
147                nc.showLoginFailedNotification(account.mId);
148            }
149            sendMailboxStatus(folder, e.getExceptionType());
150            throw e;
151        }
152    }
153
154    /**
155     * Lightweight record for the first pass of message sync, where I'm just
156     * seeing if the local message requires sync. Later (for messages that need
157     * syncing) we'll do a full readout from the DB.
158     */
159    private static class LocalMessageInfo {
160        private static final int COLUMN_ID = 0;
161        private static final int COLUMN_FLAG_LOADED = 1;
162        private static final int COLUMN_SERVER_ID = 2;
163        private static final String[] PROJECTION = new String[] {
164                EmailContent.RECORD_ID, MessageColumns.FLAG_LOADED, SyncColumns.SERVER_ID
165        };
166
167        final long mId;
168        final int mFlagLoaded;
169        final String mServerId;
170
171        public LocalMessageInfo(Cursor c) {
172            mId = c.getLong(COLUMN_ID);
173            mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED);
174            mServerId = c.getString(COLUMN_SERVER_ID);
175            // Note: mailbox key and account key not needed - they are projected
176            // for the SELECT
177        }
178    }
179
180    /**
181     * Load the structure and body of messages not yet synced
182     *
183     * @param account the account we're syncing
184     * @param remoteFolder the (open) Folder we're working on
185     * @param unsyncedMessages an array of Message's we've got headers for
186     * @param toMailbox the destination mailbox we're syncing
187     * @throws MessagingException
188     */
189    static void loadUnsyncedMessages(final Context context, final Account account,
190            Pop3Folder remoteFolder, ArrayList<Pop3Message> unsyncedMessages,
191            final Mailbox toMailbox) throws MessagingException {
192        if (MailActivityEmail.DEBUG) {
193            Log.d(TAG, "Loading " + unsyncedMessages.size() + " unsynced messages");
194        }
195        try {
196            int cnt = unsyncedMessages.size();
197            // We'll load them from most recent to oldest
198            for (int i = cnt - 1; i >= 0; i--) {
199                Pop3Message message = unsyncedMessages.get(i);
200                remoteFolder.fetchBody(message, Pop3Store.FETCH_BODY_SANE_SUGGESTED_SIZE / 76);
201                int flag = EmailContent.Message.FLAG_LOADED_COMPLETE;
202                if (!message.isComplete()) {
203                     flag = EmailContent.Message.FLAG_LOADED_UNKNOWN;
204                }
205                if (MailActivityEmail.DEBUG) {
206                    Log.d(TAG, "Message is " + (message.isComplete() ? "" : "NOT ") + "complete");
207                }
208                // If message is incomplete, create a "fake" attachment
209                Utilities.copyOneMessageToProvider(context, message, account, toMailbox, flag);
210            }
211        } catch (IOException e) {
212            throw new MessagingException(MessagingException.IOERROR);
213        }
214    }
215
216    /**
217     * Synchronizer
218     *
219     * @param account the account to sync
220     * @param mailbox the mailbox to sync
221     * @throws MessagingException
222     */
223    private static void synchronizePop3Mailbox(final Context context,
224            final Account account, final Mailbox mailbox) throws MessagingException {
225        // TODO Break this into smaller pieces
226        ContentResolver resolver = context.getContentResolver();
227
228        // We only sync Inbox
229        if (mailbox.mType != Mailbox.TYPE_INBOX) {
230            return;
231        }
232
233        // Get the message list from EmailProvider and create an index of the uids
234
235        Cursor localUidCursor = null;
236        HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>();
237
238        try {
239            localUidCursor = resolver.query(
240                    EmailContent.Message.CONTENT_URI,
241                    LocalMessageInfo.PROJECTION,
242                    EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
243                            " AND " + MessageColumns.MAILBOX_KEY + "=?",
244                    new String[] {
245                            String.valueOf(account.mId),
246                            String.valueOf(mailbox.mId)
247                    },
248                    null);
249            while (localUidCursor.moveToNext()) {
250                LocalMessageInfo info = new LocalMessageInfo(localUidCursor);
251                localMessageMap.put(info.mServerId, info);
252            }
253        } finally {
254            if (localUidCursor != null) {
255                localUidCursor.close();
256            }
257        }
258
259        // Open the remote folder and create the remote folder if necessary
260
261        Pop3Store remoteStore = (Pop3Store)Store.getInstance(account, context);
262        // The account might have been deleted
263        if (remoteStore == null)
264            return;
265        Pop3Folder remoteFolder = (Pop3Folder)remoteStore.getFolder(mailbox.mServerId);
266
267        // Open the remote folder. This pre-loads certain metadata like message
268        // count.
269        remoteFolder.open(OpenMode.READ_WRITE);
270
271        // Get the remote message count.
272        int remoteMessageCount = remoteFolder.getMessageCount();
273        ContentValues values = new ContentValues();
274        values.put(MailboxColumns.TOTAL_COUNT, remoteMessageCount);
275        mailbox.update(context, values);
276
277        String[] accountIdArgs = new String[] { Long.toString(account.mId) };
278        long trashMailboxId = Mailbox.findMailboxOfType(context, account.mId, Mailbox.TYPE_TRASH);
279        Cursor updates = resolver.query(
280                EmailContent.Message.UPDATED_CONTENT_URI,
281                EmailContent.Message.ID_COLUMN_PROJECTION,
282                EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs,
283                null);
284        try {
285            // loop through messages marked as deleted
286            while (updates.moveToNext()) {
287                long id = updates.getLong(Message.ID_COLUMNS_ID_COLUMN);
288                EmailContent.Message currentMsg =
289                        EmailContent.Message.restoreMessageWithId(context, id);
290                if (currentMsg.mMailboxKey == trashMailboxId) {
291                    // Delete this on the server
292                    Pop3Message popMessage =
293                            (Pop3Message)remoteFolder.getMessage(currentMsg.mServerId);
294                    if (popMessage != null) {
295                        remoteFolder.deleteMessage(popMessage);
296                    }
297                }
298                // Finally, delete the update
299                Uri uri = ContentUris.withAppendedId(EmailContent.Message.UPDATED_CONTENT_URI, id);
300                context.getContentResolver().delete(uri, null, null);
301            }
302        } finally {
303            updates.close();
304        }
305
306        // Determine the limit # of messages to download
307        int visibleLimit = mailbox.mVisibleLimit;
308        if (visibleLimit <= 0) {
309            visibleLimit = MailActivityEmail.VISIBLE_LIMIT_DEFAULT;
310        }
311
312        // Create a list of messages to download
313        Pop3Message[] remoteMessages = new Pop3Message[0];
314        final ArrayList<Pop3Message> unsyncedMessages = new ArrayList<Pop3Message>();
315        HashMap<String, Pop3Message> remoteUidMap = new HashMap<String, Pop3Message>();
316
317        if (remoteMessageCount > 0) {
318            /*
319             * Message numbers start at 1.
320             */
321            remoteMessages = remoteFolder.getMessages(remoteMessageCount, visibleLimit);
322
323            /*
324             * Get a list of the messages that are in the remote list but not on
325             * the local store, or messages that are in the local store but
326             * failed to download on the last sync. These are the new messages
327             * that we will download. Note, we also skip syncing messages which
328             * are flagged as "deleted message" sentinels, because they are
329             * locally deleted and we don't need or want the old message from
330             * the server.
331             */
332            for (Pop3Message message : remoteMessages) {
333                String uid = message.getUid();
334                remoteUidMap.put(uid, message);
335                LocalMessageInfo localMessage = localMessageMap.get(uid);
336                // localMessage == null -> message has never been created (not even headers)
337                // mFlagLoaded = UNLOADED -> message created, but none of body loaded
338                if (localMessage == null ||
339                        (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_UNLOADED)) {
340                    unsyncedMessages.add(message);
341                }
342            }
343        } else {
344            if (MailActivityEmail.DEBUG) {
345                Log.d(TAG, "*** Message count is zero??");
346            }
347            return;
348        }
349
350        // Get "attachments" to be loaded
351        Cursor c = resolver.query(Attachment.CONTENT_URI, Attachment.CONTENT_PROJECTION,
352                AttachmentColumns.ACCOUNT_KEY + "=? AND " +
353                        AttachmentColumns.UI_STATE + "=" + AttachmentState.DOWNLOADING,
354                new String[] {Long.toString(account.mId)}, null);
355        try {
356            values.clear();
357            while (c.moveToNext()) {
358                values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.SAVED);
359                Attachment att = new Attachment();
360                att.restore(c);
361                Message msg = Message.restoreMessageWithId(context, att.mMessageKey);
362                if (msg == null || (msg.mFlagLoaded == Message.FLAG_LOADED_COMPLETE)) {
363                    values.put(AttachmentColumns.UI_DOWNLOADED_SIZE, att.mSize);
364                    resolver.update(ContentUris.withAppendedId(Attachment.CONTENT_URI, att.mId),
365                            values, null, null);
366                    continue;
367                } else {
368                    String uid = msg.mServerId;
369                    Pop3Message popMessage = remoteUidMap.get(uid);
370                    if (popMessage != null) {
371                        try {
372                            remoteFolder.fetchBody(popMessage, -1);
373                        } catch (IOException e) {
374                            throw new MessagingException(MessagingException.IOERROR);
375                        }
376
377                        // Say we've downloaded the attachment
378                        values.put(AttachmentColumns.UI_STATE, AttachmentState.SAVED);
379                        Uri attUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, att.mId);
380                        resolver.update(attUri, values, null, null);
381
382                        int flag = EmailContent.Message.FLAG_LOADED_COMPLETE;
383                        if (!popMessage.isComplete()) {
384                            Log.e(TAG, "How is this possible?");
385                        }
386                        Utilities.copyOneMessageToProvider(
387                                context, popMessage, account, mailbox, flag);
388                        // Get rid of the temporary attachment
389                        resolver.delete(attUri, null, null);
390
391                    }
392                }
393            }
394        } finally {
395            c.close();
396        }
397
398        // Remove any messages that are in the local store but no longer on the remote store.
399        HashSet<String> localUidsToDelete = new HashSet<String>(localMessageMap.keySet());
400        localUidsToDelete.removeAll(remoteUidMap.keySet());
401        for (String uidToDelete : localUidsToDelete) {
402            LocalMessageInfo infoToDelete = localMessageMap.get(uidToDelete);
403
404            // Delete associated data (attachment files)
405            // Attachment & Body records are auto-deleted when we delete the
406            // Message record
407            AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId,
408                    infoToDelete.mId);
409
410            // Delete the message itself
411            Uri uriToDelete = ContentUris.withAppendedId(
412                    EmailContent.Message.CONTENT_URI, infoToDelete.mId);
413            resolver.delete(uriToDelete, null, null);
414
415            // Delete extra rows (e.g. synced or deleted)
416            Uri updateRowToDelete = ContentUris.withAppendedId(
417                    EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
418            resolver.delete(updateRowToDelete, null, null);
419            Uri deleteRowToDelete = ContentUris.withAppendedId(
420                    EmailContent.Message.DELETED_CONTENT_URI, infoToDelete.mId);
421            resolver.delete(deleteRowToDelete, null, null);
422        }
423
424        // Load messages we need to sync
425        loadUnsyncedMessages(context, account, remoteFolder, unsyncedMessages, mailbox);
426
427        // Clean up and report results
428        remoteFolder.close(false);
429    }
430}
431