Home | History | Annotate | Download | only in service
      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 
     17 package com.android.email.service;
     18 
     19 import android.app.Service;
     20 import android.content.ContentResolver;
     21 import android.content.ContentUris;
     22 import android.content.ContentValues;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.database.Cursor;
     26 import android.net.TrafficStats;
     27 import android.net.Uri;
     28 import android.os.IBinder;
     29 import android.os.RemoteException;
     30 
     31 import com.android.email.DebugUtils;
     32 import com.android.email.NotificationController;
     33 import com.android.email.NotificationControllerCreatorHolder;
     34 import com.android.email.mail.Store;
     35 import com.android.email.mail.store.Pop3Store;
     36 import com.android.email.mail.store.Pop3Store.Pop3Folder;
     37 import com.android.email.mail.store.Pop3Store.Pop3Message;
     38 import com.android.email.provider.Utilities;
     39 import com.android.emailcommon.Logging;
     40 import com.android.emailcommon.TrafficFlags;
     41 import com.android.emailcommon.mail.AuthenticationFailedException;
     42 import com.android.emailcommon.mail.Folder.OpenMode;
     43 import com.android.emailcommon.mail.MessagingException;
     44 import com.android.emailcommon.provider.Account;
     45 import com.android.emailcommon.provider.EmailContent;
     46 import com.android.emailcommon.provider.EmailContent.Attachment;
     47 import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
     48 import com.android.emailcommon.provider.EmailContent.Message;
     49 import com.android.emailcommon.provider.EmailContent.MessageColumns;
     50 import com.android.emailcommon.provider.EmailContent.SyncColumns;
     51 import com.android.emailcommon.provider.Mailbox;
     52 import com.android.emailcommon.service.EmailServiceStatus;
     53 import com.android.emailcommon.service.IEmailServiceCallback;
     54 import com.android.emailcommon.utility.AttachmentUtilities;
     55 import com.android.mail.providers.UIProvider;
     56 import com.android.mail.providers.UIProvider.AttachmentState;
     57 import com.android.mail.utils.LogUtils;
     58 
     59 import org.apache.james.mime4j.EOLConvertingInputStream;
     60 
     61 import java.io.IOException;
     62 import java.util.ArrayList;
     63 import java.util.HashMap;
     64 import java.util.HashSet;
     65 
     66 public class Pop3Service extends Service {
     67     private static final String TAG = "Pop3Service";
     68     private static final int DEFAULT_SYNC_COUNT = 100;
     69 
     70     @Override
     71     public int onStartCommand(Intent intent, int flags, int startId) {
     72         return Service.START_STICKY;
     73     }
     74 
     75     /**
     76      * Create our EmailService implementation here.
     77      */
     78     private final EmailServiceStub mBinder = new EmailServiceStub() {
     79         @Override
     80         public void loadAttachment(final IEmailServiceCallback callback, final long accountId,
     81                 final long attachmentId, final boolean background) throws RemoteException {
     82             Attachment att = Attachment.restoreAttachmentWithId(mContext, attachmentId);
     83             if (att == null || att.mUiState != AttachmentState.DOWNLOADING) return;
     84             long inboxId = Mailbox.findMailboxOfType(mContext, att.mAccountKey, Mailbox.TYPE_INBOX);
     85             if (inboxId == Mailbox.NO_MAILBOX) return;
     86             // We load attachments during a sync
     87             requestSync(inboxId, true, 0);
     88         }
     89     };
     90 
     91     @Override
     92     public IBinder onBind(Intent intent) {
     93         mBinder.init(this);
     94         return mBinder;
     95     }
     96 
     97     /**
     98      * Start foreground synchronization of the specified folder. This is called
     99      * by synchronizeMailbox or checkMail. TODO this should use ID's instead of
    100      * fully-restored objects
    101      *
    102      * @param account
    103      * @param folder
    104      * @param deltaMessageCount the requested change in number of messages to sync.
    105      * @return The status code for whether this operation succeeded.
    106      * @throws MessagingException
    107      */
    108     public static int synchronizeMailboxSynchronous(Context context, final Account account,
    109             final Mailbox folder, final int deltaMessageCount) throws MessagingException {
    110         TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
    111         final NotificationController nc =
    112                 NotificationControllerCreatorHolder.getInstance(context);
    113         try {
    114             synchronizePop3Mailbox(context, account, folder, deltaMessageCount);
    115             // Clear authentication notification for this account
    116             if (nc != null) {
    117                 nc.cancelLoginFailedNotification(account.mId);
    118             }
    119         } catch (MessagingException e) {
    120             if (Logging.LOGD) {
    121                 LogUtils.v(Logging.LOG_TAG, "synchronizeMailbox", e);
    122             }
    123             if (e instanceof AuthenticationFailedException && nc != null) {
    124                 // Generate authentication notification
    125                 nc.showLoginFailedNotificationSynchronous(account.mId, true /* incoming */);
    126             }
    127             throw e;
    128         }
    129         // TODO: Rather than use exceptions as logic aobve, return the status and handle it
    130         // correctly in caller.
    131         return EmailServiceStatus.SUCCESS;
    132     }
    133 
    134     /**
    135      * Lightweight record for the first pass of message sync, where I'm just
    136      * seeing if the local message requires sync. Later (for messages that need
    137      * syncing) we'll do a full readout from the DB.
    138      */
    139     private static class LocalMessageInfo {
    140         private static final int COLUMN_ID = 0;
    141         private static final int COLUMN_FLAG_LOADED = 1;
    142         private static final int COLUMN_SERVER_ID = 2;
    143         private static final String[] PROJECTION = new String[] {
    144                 EmailContent.RECORD_ID, MessageColumns.FLAG_LOADED, SyncColumns.SERVER_ID
    145         };
    146 
    147         final long mId;
    148         final int mFlagLoaded;
    149         final String mServerId;
    150 
    151         public LocalMessageInfo(Cursor c) {
    152             mId = c.getLong(COLUMN_ID);
    153             mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED);
    154             mServerId = c.getString(COLUMN_SERVER_ID);
    155             // Note: mailbox key and account key not needed - they are projected
    156             // for the SELECT
    157         }
    158     }
    159 
    160     /**
    161      * Load the structure and body of messages not yet synced
    162      *
    163      * @param account the account we're syncing
    164      * @param remoteFolder the (open) Folder we're working on
    165      * @param unsyncedMessages an array of Message's we've got headers for
    166      * @param toMailbox the destination mailbox we're syncing
    167      * @throws MessagingException
    168      */
    169     static void loadUnsyncedMessages(final Context context, final Account account,
    170             Pop3Folder remoteFolder, ArrayList<Pop3Message> unsyncedMessages,
    171             final Mailbox toMailbox) throws MessagingException {
    172 
    173         if (DebugUtils.DEBUG) {
    174             LogUtils.d(TAG, "Loading " + unsyncedMessages.size() + " unsynced messages");
    175         }
    176 
    177         try {
    178             int cnt = unsyncedMessages.size();
    179             // They are in most recent to least recent order, process them that way.
    180             for (int i = 0; i < cnt; i++) {
    181                 final Pop3Message message = unsyncedMessages.get(i);
    182                 remoteFolder.fetchBody(message, Pop3Store.FETCH_BODY_SANE_SUGGESTED_SIZE / 76,
    183                         null);
    184                 int flag = EmailContent.Message.FLAG_LOADED_COMPLETE;
    185                 if (!message.isComplete()) {
    186                     // TODO: when the message is not complete, this should mark the message as
    187                     // partial.  When that change is made, we need to make sure that:
    188                     // 1) Partial messages are shown in the conversation list
    189                     // 2) We are able to download the rest of the message/attachment when the
    190                     //    user requests it.
    191                      flag = EmailContent.Message.FLAG_LOADED_PARTIAL;
    192                 }
    193                 if (DebugUtils.DEBUG) {
    194                     LogUtils.d(TAG, "Message is " + (message.isComplete() ? "" : "NOT ")
    195                             + "complete");
    196                 }
    197                 // If message is incomplete, create a "fake" attachment
    198                 Utilities.copyOneMessageToProvider(context, message, account, toMailbox, flag);
    199             }
    200         } catch (IOException e) {
    201             throw new MessagingException(MessagingException.IOERROR);
    202         }
    203     }
    204 
    205     private static class FetchCallback implements EOLConvertingInputStream.Callback {
    206         private final ContentResolver mResolver;
    207         private final Uri mAttachmentUri;
    208         private final ContentValues mContentValues = new ContentValues();
    209 
    210         FetchCallback(ContentResolver resolver, Uri attachmentUri) {
    211             mResolver = resolver;
    212             mAttachmentUri = attachmentUri;
    213         }
    214 
    215         @Override
    216         public void report(int bytesRead) {
    217             mContentValues.put(AttachmentColumns.UI_DOWNLOADED_SIZE, bytesRead);
    218             mResolver.update(mAttachmentUri, mContentValues, null, null);
    219         }
    220     }
    221 
    222     /**
    223      * Synchronizer
    224      *
    225      * @param account the account to sync
    226      * @param mailbox the mailbox to sync
    227      * @param deltaMessageCount the requested change to number of messages to sync
    228      * @throws MessagingException
    229      */
    230     private synchronized static void synchronizePop3Mailbox(final Context context, final Account account,
    231             final Mailbox mailbox, final int deltaMessageCount) throws MessagingException {
    232         // TODO Break this into smaller pieces
    233         ContentResolver resolver = context.getContentResolver();
    234 
    235         // We only sync Inbox
    236         if (mailbox.mType != Mailbox.TYPE_INBOX) {
    237             return;
    238         }
    239 
    240         // Get the message list from EmailProvider and create an index of the uids
    241 
    242         Cursor localUidCursor = null;
    243         HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>();
    244 
    245         try {
    246             localUidCursor = resolver.query(
    247                     EmailContent.Message.CONTENT_URI,
    248                     LocalMessageInfo.PROJECTION,
    249                     MessageColumns.MAILBOX_KEY + "=?",
    250                     new String[] {
    251                             String.valueOf(mailbox.mId)
    252                     },
    253                     null);
    254             while (localUidCursor.moveToNext()) {
    255                 LocalMessageInfo info = new LocalMessageInfo(localUidCursor);
    256                 localMessageMap.put(info.mServerId, info);
    257             }
    258         } finally {
    259             if (localUidCursor != null) {
    260                 localUidCursor.close();
    261             }
    262         }
    263 
    264         // Open the remote folder and create the remote folder if necessary
    265 
    266         Pop3Store remoteStore = (Pop3Store)Store.getInstance(account, context);
    267         // The account might have been deleted
    268         if (remoteStore == null)
    269             return;
    270         Pop3Folder remoteFolder = (Pop3Folder)remoteStore.getFolder(mailbox.mServerId);
    271 
    272         // Open the remote folder. This pre-loads certain metadata like message
    273         // count.
    274         remoteFolder.open(OpenMode.READ_WRITE);
    275 
    276         String[] accountIdArgs = new String[] { Long.toString(account.mId) };
    277         long trashMailboxId = Mailbox.findMailboxOfType(context, account.mId, Mailbox.TYPE_TRASH);
    278         Cursor updates = resolver.query(
    279                 EmailContent.Message.UPDATED_CONTENT_URI,
    280                 EmailContent.Message.ID_COLUMN_PROJECTION,
    281                 EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs,
    282                 null);
    283         try {
    284             // loop through messages marked as deleted
    285             while (updates.moveToNext()) {
    286                 long id = updates.getLong(Message.ID_COLUMNS_ID_COLUMN);
    287                 EmailContent.Message currentMsg =
    288                         EmailContent.Message.restoreMessageWithId(context, id);
    289                 if (currentMsg.mMailboxKey == trashMailboxId) {
    290                     // Delete this on the server
    291                     Pop3Message popMessage =
    292                             (Pop3Message)remoteFolder.getMessage(currentMsg.mServerId);
    293                     if (popMessage != null) {
    294                         remoteFolder.deleteMessage(popMessage);
    295                     }
    296                 }
    297                 // Finally, delete the update
    298                 Uri uri = ContentUris.withAppendedId(EmailContent.Message.UPDATED_CONTENT_URI, id);
    299                 context.getContentResolver().delete(uri, null, null);
    300             }
    301         } finally {
    302             updates.close();
    303         }
    304 
    305         // Get the remote message count.
    306         final int remoteMessageCount = remoteFolder.getMessageCount();
    307 
    308         // Save the folder message count.
    309         mailbox.updateMessageCount(context, remoteMessageCount);
    310 
    311         // Create a list of messages to download
    312         Pop3Message[] remoteMessages = new Pop3Message[0];
    313         final ArrayList<Pop3Message> unsyncedMessages = new ArrayList<Pop3Message>();
    314         HashMap<String, Pop3Message> remoteUidMap = new HashMap<String, Pop3Message>();
    315 
    316         if (remoteMessageCount > 0) {
    317             /*
    318              * Get all messageIds in the mailbox.
    319              * We don't necessarily need to sync all of them.
    320              */
    321             remoteMessages = remoteFolder.getMessages(remoteMessageCount, remoteMessageCount);
    322             LogUtils.d(Logging.LOG_TAG, "remoteMessageCount " + remoteMessageCount);
    323 
    324             /*
    325              * TODO: It would be nicer if the default sync window were time based rather than
    326              * count based, but POP3 does not support time based queries, and the UIDL command
    327              * does not report timestamps. To handle this, we would need to load a block of
    328              * Ids, sync those messages to get the timestamps, and then load more Ids until we
    329              * have filled out our window.
    330              */
    331             int count = 0;
    332             int countNeeded = DEFAULT_SYNC_COUNT;
    333             for (final Pop3Message message : remoteMessages) {
    334                 final String uid = message.getUid();
    335                 remoteUidMap.put(uid, message);
    336             }
    337 
    338             /*
    339              * Figure out which messages we need to sync. Start at the most recent ones, and keep
    340              * going until we hit one of four end conditions:
    341              * 1. We currently have zero local messages. In this case, we will sync the most recent
    342              * DEFAULT_SYNC_COUNT, then stop.
    343              * 2. We have some local messages, and after encountering them, we find some older
    344              * messages that do not yet exist locally. In this case, we will load whichever came
    345              * before the ones we already had locally, and also deltaMessageCount additional
    346              * older messages.
    347              * 3. We have some local messages, but after examining the most recent
    348              * DEFAULT_SYNC_COUNT remote messages, we still have not encountered any that exist
    349              * locally. In this case, we'll stop adding new messages to sync, leaving a gap between
    350              * the ones we've just loaded and the ones we already had.
    351              * 4. We examine all of the remote messages before running into any of our count
    352              * limitations.
    353              */
    354             for (final Pop3Message message : remoteMessages) {
    355                 final String uid = message.getUid();
    356                 final LocalMessageInfo localMessage = localMessageMap.get(uid);
    357                 if (localMessage == null) {
    358                     count++;
    359                 } else {
    360                     // We have found a message that already exists locally. We may or may not
    361                     // need to keep looking, depending on what deltaMessageCount is.
    362                     LogUtils.d(Logging.LOG_TAG, "found a local message, need " +
    363                             deltaMessageCount + " more remote messages");
    364                     countNeeded = deltaMessageCount;
    365                     count = 0;
    366                 }
    367 
    368                 // localMessage == null -> message has never been created (not even headers)
    369                 // mFlagLoaded != FLAG_LOADED_COMPLETE -> message failed to sync completely
    370                 if (localMessage == null ||
    371                         (localMessage.mFlagLoaded != EmailContent.Message.FLAG_LOADED_COMPLETE &&
    372                                 localMessage.mFlagLoaded != Message.FLAG_LOADED_PARTIAL)) {
    373                     LogUtils.d(Logging.LOG_TAG, "need to sync " + uid);
    374                     unsyncedMessages.add(message);
    375                 } else {
    376                     LogUtils.d(Logging.LOG_TAG, "don't need to sync " + uid);
    377                 }
    378 
    379                 if (count >= countNeeded) {
    380                     LogUtils.d(Logging.LOG_TAG, "loaded " + count + " messages, stopping");
    381                     break;
    382                 }
    383             }
    384         } else {
    385             if (DebugUtils.DEBUG) {
    386                 LogUtils.d(TAG, "*** Message count is zero??");
    387             }
    388             remoteFolder.close(false);
    389             return;
    390         }
    391 
    392         // Get "attachments" to be loaded
    393         Cursor c = resolver.query(Attachment.CONTENT_URI, Attachment.CONTENT_PROJECTION,
    394                 AttachmentColumns.ACCOUNT_KEY + "=? AND " +
    395                         AttachmentColumns.UI_STATE + "=" + AttachmentState.DOWNLOADING,
    396                 new String[] {Long.toString(account.mId)}, null);
    397         try {
    398             final ContentValues values = new ContentValues();
    399             while (c.moveToNext()) {
    400                 values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.SAVED);
    401                 Attachment att = new Attachment();
    402                 att.restore(c);
    403                 Message msg = Message.restoreMessageWithId(context, att.mMessageKey);
    404                 if (msg == null || (msg.mFlagLoaded == Message.FLAG_LOADED_COMPLETE)) {
    405                     values.put(AttachmentColumns.UI_DOWNLOADED_SIZE, att.mSize);
    406                     resolver.update(ContentUris.withAppendedId(Attachment.CONTENT_URI, att.mId),
    407                             values, null, null);
    408                     continue;
    409                 } else {
    410                     String uid = msg.mServerId;
    411                     Pop3Message popMessage = remoteUidMap.get(uid);
    412                     if (popMessage != null) {
    413                         Uri attUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, att.mId);
    414                         try {
    415                             remoteFolder.fetchBody(popMessage, -1,
    416                                     new FetchCallback(resolver, attUri));
    417                         } catch (IOException e) {
    418                             throw new MessagingException(MessagingException.IOERROR);
    419                         }
    420 
    421                         // Say we've downloaded the attachment
    422                         values.put(AttachmentColumns.UI_STATE, AttachmentState.SAVED);
    423                         resolver.update(attUri, values, null, null);
    424 
    425                         int flag = EmailContent.Message.FLAG_LOADED_COMPLETE;
    426                         if (!popMessage.isComplete()) {
    427                             LogUtils.e(TAG, "How is this possible?");
    428                         }
    429                         Utilities.copyOneMessageToProvider(
    430                                 context, popMessage, account, mailbox, flag);
    431                         // Get rid of the temporary attachment
    432                         resolver.delete(attUri, null, null);
    433 
    434                     } else {
    435                         // TODO: Should we mark this attachment as failed so we don't
    436                         // keep trying to download?
    437                         LogUtils.e(TAG, "Could not find message for attachment " + uid);
    438                     }
    439                 }
    440             }
    441         } finally {
    442             c.close();
    443         }
    444 
    445         // Remove any messages that are in the local store but no longer on the remote store.
    446         HashSet<String> localUidsToDelete = new HashSet<String>(localMessageMap.keySet());
    447         localUidsToDelete.removeAll(remoteUidMap.keySet());
    448         for (String uidToDelete : localUidsToDelete) {
    449             LogUtils.d(Logging.LOG_TAG, "need to delete " + uidToDelete);
    450             LocalMessageInfo infoToDelete = localMessageMap.get(uidToDelete);
    451 
    452             // Delete associated data (attachment files)
    453             // Attachment & Body records are auto-deleted when we delete the
    454             // Message record
    455             AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId,
    456                     infoToDelete.mId);
    457 
    458             // Delete the message itself
    459             Uri uriToDelete = ContentUris.withAppendedId(
    460                     EmailContent.Message.CONTENT_URI, infoToDelete.mId);
    461             resolver.delete(uriToDelete, null, null);
    462 
    463             // Delete extra rows (e.g. synced or deleted)
    464             Uri updateRowToDelete = ContentUris.withAppendedId(
    465                     EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
    466             resolver.delete(updateRowToDelete, null, null);
    467             Uri deleteRowToDelete = ContentUris.withAppendedId(
    468                     EmailContent.Message.DELETED_CONTENT_URI, infoToDelete.mId);
    469             resolver.delete(deleteRowToDelete, null, null);
    470         }
    471 
    472         LogUtils.d(TAG, "loadUnsynchedMessages " + unsyncedMessages.size());
    473         // Load messages we need to sync
    474         loadUnsyncedMessages(context, account, remoteFolder, unsyncedMessages, mailbox);
    475 
    476         // Clean up and report results
    477         remoteFolder.close(false);
    478     }
    479 }
    480