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