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