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