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 onStartCommand(Intent intent, int flags, int startId)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 onBind(Intent intent)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 */ synchronizeMailboxSynchronous(Context context, final Account account, final Mailbox folder, final int deltaMessageCount)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 LocalMessageInfo(Cursor c)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 */ loadUnsyncedMessages(final Context context, final Account account, Pop3Folder remoteFolder, ArrayList<Pop3Message> unsyncedMessages, final Mailbox toMailbox)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 FetchCallback(ContentResolver resolver, Uri attachmentUri)210 FetchCallback(ContentResolver resolver, Uri attachmentUri) { 211 mResolver = resolver; 212 mAttachmentUri = attachmentUri; 213 } 214 215 @Override report(int bytesRead)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 */ synchronizePop3Mailbox(final Context context, final Account account, final Mailbox mailbox, final int deltaMessageCount)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