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