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