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