EmailSyncAdapter.java revision 77424af660458104b732bdcb718874b17d0cab3a
1/* 2 * Copyright (C) 2008-2009 Marc Blank 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.android.exchange.adapter; 19 20import com.android.email.R; 21import com.android.email.activity.MessageList; 22import com.android.email.mail.Address; 23import com.android.email.provider.EmailContent; 24import com.android.email.provider.EmailProvider; 25import com.android.email.provider.EmailContent.Attachment; 26import com.android.email.provider.EmailContent.Mailbox; 27import com.android.email.provider.EmailContent.Message; 28import com.android.email.provider.EmailContent.MessageColumns; 29import com.android.email.provider.EmailContent.SyncColumns; 30import com.android.exchange.Eas; 31import com.android.exchange.EasSyncService; 32 33import android.app.Notification; 34import android.app.NotificationManager; 35import android.app.PendingIntent; 36import android.content.ContentProviderOperation; 37import android.content.ContentResolver; 38import android.content.ContentUris; 39import android.content.ContentValues; 40import android.content.Context; 41import android.content.Intent; 42import android.content.OperationApplicationException; 43import android.database.Cursor; 44import android.net.Uri; 45import android.os.RemoteException; 46import android.text.TextUtils; 47import android.webkit.MimeTypeMap; 48 49import java.io.IOException; 50import java.io.InputStream; 51import java.util.ArrayList; 52import java.util.GregorianCalendar; 53import java.util.TimeZone; 54 55/** 56 * Sync adapter for EAS email 57 * 58 */ 59public class EmailSyncAdapter extends AbstractSyncAdapter { 60 61 private static final int UPDATES_READ_COLUMN = 0; 62 private static final int UPDATES_MAILBOX_KEY_COLUMN = 1; 63 private static final int UPDATES_SERVER_ID_COLUMN = 2; 64 private static final String[] UPDATES_PROJECTION = 65 {MessageColumns.FLAG_READ, MessageColumns.MAILBOX_KEY, SyncColumns.SERVER_ID}; 66 67 String[] bindArguments = new String[2]; 68 69 ArrayList<Long> mDeletedIdList = new ArrayList<Long>(); 70 ArrayList<Long> mUpdatedIdList = new ArrayList<Long>(); 71 72 public EmailSyncAdapter(Mailbox mailbox, EasSyncService service) { 73 super(mailbox, service); 74 } 75 76 @Override 77 public boolean parse(InputStream is, EasSyncService service) throws IOException { 78 EasEmailSyncParser p = new EasEmailSyncParser(is, service); 79 return p.parse(); 80 } 81 82 public class EasEmailSyncParser extends AbstractSyncParser { 83 84 private static final String WHERE_SERVER_ID_AND_MAILBOX_KEY = 85 SyncColumns.SERVER_ID + "=? and " + MessageColumns.MAILBOX_KEY + "=?"; 86 87 private String mMailboxIdAsString; 88 89 public EasEmailSyncParser(InputStream in, EasSyncService service) throws IOException { 90 super(in, service); 91 mMailboxIdAsString = Long.toString(mMailbox.mId); 92 } 93 94 @Override 95 public void wipe() { 96 mContentResolver.delete(Message.CONTENT_URI, 97 Message.MAILBOX_KEY + "=" + mMailbox.mId, null); 98 mContentResolver.delete(Message.DELETED_CONTENT_URI, 99 Message.MAILBOX_KEY + "=" + mMailbox.mId, null); 100 mContentResolver.delete(Message.UPDATED_CONTENT_URI, 101 Message.MAILBOX_KEY + "=" + mMailbox.mId, null); 102 } 103 104 public void addData (Message msg) throws IOException { 105 ArrayList<Attachment> atts = new ArrayList<Attachment>(); 106 107 while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) { 108 switch (tag) { 109 case Tags.EMAIL_ATTACHMENTS: 110 attachmentsParser(atts, msg); 111 break; 112 case Tags.EMAIL_TO: 113 msg.mTo = Address.pack(Address.parse(getValue())); 114 break; 115 case Tags.EMAIL_FROM: 116 String from = getValue(); 117 String sender = from; 118 int q = from.indexOf('\"'); 119 if (q >= 0) { 120 int qq = from.indexOf('\"', q + 1); 121 if (qq > 0) { 122 sender = from.substring(q + 1, qq); 123 } 124 } 125 msg.mDisplayName = sender; 126 msg.mFrom = Address.pack(Address.parse(from)); 127 break; 128 case Tags.EMAIL_CC: 129 msg.mCc = Address.pack(Address.parse(getValue())); 130 break; 131 case Tags.EMAIL_REPLY_TO: 132 msg.mReplyTo = Address.pack(Address.parse(getValue())); 133 break; 134 case Tags.EMAIL_DATE_RECEIVED: 135 String date = getValue(); 136 // 2009-02-11T18:03:03.627Z 137 GregorianCalendar cal = new GregorianCalendar(); 138 cal.set(Integer.parseInt(date.substring(0, 4)), Integer.parseInt(date 139 .substring(5, 7)) - 1, Integer.parseInt(date.substring(8, 10)), 140 Integer.parseInt(date.substring(11, 13)), Integer.parseInt(date 141 .substring(14, 16)), Integer.parseInt(date 142 .substring(17, 19))); 143 cal.setTimeZone(TimeZone.getTimeZone("GMT")); 144 msg.mTimeStamp = cal.getTimeInMillis(); 145 break; 146 case Tags.EMAIL_SUBJECT: 147 msg.mSubject = getValue(); 148 break; 149 case Tags.EMAIL_READ: 150 msg.mFlagRead = getValueInt() == 1; 151 break; 152 case Tags.BASE_BODY: 153 bodyParser(msg); 154 break; 155 case Tags.EMAIL_FLAG: 156 msg.mFlagFavorite = flagParser(); 157 break; 158 case Tags.EMAIL_BODY: 159 String text = getValue(); 160 msg.mText = text; 161 msg.mTextInfo = "X;X;8;" + text.length(); // location;encoding;charset;size 162 break; 163 default: 164 skipTag(); 165 } 166 } 167 168 if (atts.size() > 0) { 169 msg.mAttachments = atts; 170 } 171 } 172 173 private void addParser(ArrayList<Message> emails) throws IOException { 174 Message msg = new Message(); 175 msg.mAccountKey = mAccount.mId; 176 msg.mMailboxKey = mMailbox.mId; 177 msg.mFlagLoaded = Message.LOADED; 178 179 while (nextTag(Tags.SYNC_ADD) != END) { 180 switch (tag) { 181 case Tags.SYNC_SERVER_ID: 182 msg.mServerId = getValue(); 183 break; 184 case Tags.SYNC_APPLICATION_DATA: 185 addData(msg); 186 break; 187 default: 188 skipTag(); 189 } 190 } 191 192 // Tell the provider that this is synced back 193 msg.mServerVersion = mMailbox.mSyncKey; 194 emails.add(msg); 195 } 196 197 // For now, we only care about the "active" state 198 private Boolean flagParser() throws IOException { 199 Boolean state = false; 200 while (nextTag(Tags.EMAIL_FLAG) != END) { 201 switch (tag) { 202 case Tags.EMAIL_FLAG_STATUS: 203 state = true; 204 break; 205 default: 206 skipTag(); 207 } 208 } 209 return state; 210 } 211 212 private void bodyParser(Message msg) throws IOException { 213 String bodyType = Eas.BODY_PREFERENCE_TEXT; 214 String body = ""; 215 while (nextTag(Tags.EMAIL_BODY) != END) { 216 switch (tag) { 217 case Tags.BASE_TYPE: 218 bodyType = getValue(); 219 break; 220 case Tags.BASE_DATA: 221 body = getValue(); 222 break; 223 default: 224 skipTag(); 225 } 226 } 227 // We always ask for TEXT or HTML; there's no third option 228 String info = "X;X;8;" + body.length(); 229 if (bodyType.equals(Eas.BODY_PREFERENCE_HTML)) { 230 msg.mHtmlInfo = info; 231 msg.mHtml = body; 232 } else { 233 msg.mTextInfo = info; 234 msg.mText = body; 235 } 236 } 237 238 private void attachmentParser(ArrayList<Attachment> atts, Message msg) throws IOException { 239 String fileName = null; 240 String length = null; 241 String location = null; 242 243 while (nextTag(Tags.EMAIL_ATTACHMENT) != END) { 244 switch (tag) { 245 case Tags.EMAIL_DISPLAY_NAME: 246 fileName = getValue(); 247 break; 248 case Tags.EMAIL_ATT_NAME: 249 location = getValue(); 250 break; 251 case Tags.EMAIL_ATT_SIZE: 252 length = getValue(); 253 break; 254 default: 255 skipTag(); 256 } 257 } 258 259 if (fileName != null && length != null && location != null) { 260 Attachment att = new Attachment(); 261 att.mEncoding = "base64"; 262 att.mSize = Long.parseLong(length); 263 att.mFileName = fileName; 264 att.mLocation = location; 265 att.mMimeType = getMimeTypeFromFileName(fileName); 266 atts.add(att); 267 msg.mFlagAttachment = true; 268 } 269 } 270 271 /** 272 * Try to determine a mime type from a file name, defaulting to application/x, where x 273 * is either the extension or (if none) octet-stream 274 * At the moment, this is somewhat lame, since many file types aren't recognized 275 * @param fileName the file name to ponder 276 * @return 277 */ 278 // Note: The MimeTypeMap method currently uses a very limited set of mime types 279 // A bug has been filed against this issue. 280 public String getMimeTypeFromFileName(String fileName) { 281 String mimeType; 282 int lastDot = fileName.lastIndexOf('.'); 283 String extension = null; 284 if (lastDot > 0 && lastDot < fileName.length() - 1) { 285 extension = fileName.substring(lastDot + 1); 286 } 287 if (extension == null) { 288 // A reasonable default for now. 289 mimeType = "application/octet-stream"; 290 } else { 291 mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 292 if (mimeType == null) { 293 mimeType = "application/" + extension; 294 } 295 } 296 return mimeType; 297 } 298 299 private void attachmentsParser(ArrayList<Attachment> atts, Message msg) throws IOException { 300 while (nextTag(Tags.EMAIL_ATTACHMENTS) != END) { 301 switch (tag) { 302 case Tags.EMAIL_ATTACHMENT: 303 attachmentParser(atts, msg); 304 break; 305 default: 306 skipTag(); 307 } 308 } 309 } 310 311 private Cursor getServerIdCursor(String serverId, String[] projection) { 312 bindArguments[0] = serverId; 313 bindArguments[1] = mMailboxIdAsString; 314 return mContentResolver.query(Message.CONTENT_URI, projection, 315 WHERE_SERVER_ID_AND_MAILBOX_KEY, bindArguments, null); 316 } 317 318 private void deleteParser(ArrayList<Long> deletes) throws IOException { 319 while (nextTag(Tags.SYNC_DELETE) != END) { 320 switch (tag) { 321 case Tags.SYNC_SERVER_ID: 322 String serverId = getValue(); 323 // Find the message in this mailbox with the given serverId 324 Cursor c = getServerIdCursor(serverId, Message.ID_COLUMN_PROJECTION); 325 try { 326 if (c.moveToFirst()) { 327 mService.userLog("Deleting " + serverId); 328 deletes.add(c.getLong(Message.ID_COLUMNS_ID_COLUMN)); 329 } 330 } finally { 331 c.close(); 332 } 333 break; 334 default: 335 skipTag(); 336 } 337 } 338 } 339 340 class ServerChange { 341 long id; 342 Boolean read; 343 Boolean flag; 344 345 ServerChange(long _id, Boolean _read, Boolean _flag) { 346 id = _id; 347 read = _read; 348 flag = _flag; 349 } 350 } 351 352 private void changeParser(ArrayList<ServerChange> changes) throws IOException { 353 String serverId = null; 354 Boolean oldRead = false; 355 Boolean read = null; 356 Boolean oldFlag = false; 357 Boolean flag = null; 358 long id = 0; 359 while (nextTag(Tags.SYNC_CHANGE) != END) { 360 switch (tag) { 361 case Tags.SYNC_SERVER_ID: 362 serverId = getValue(); 363 Cursor c = getServerIdCursor(serverId, Message.LIST_PROJECTION); 364 try { 365 if (c.moveToFirst()) { 366 mService.userLog("Changing " + serverId); 367 oldRead = c.getInt(Message.LIST_READ_COLUMN) == Message.READ; 368 oldFlag = c.getInt(Message.LIST_FAVORITE_COLUMN) == 1; 369 id = c.getLong(Message.LIST_ID_COLUMN); 370 } 371 } finally { 372 c.close(); 373 } 374 break; 375 case Tags.EMAIL_READ: 376 read = getValueInt() == 1; 377 break; 378 case Tags.EMAIL_FLAG: 379 flag = flagParser(); 380 break; 381 case Tags.SYNC_APPLICATION_DATA: 382 break; 383 default: 384 skipTag(); 385 } 386 } 387 if ((read != null && !oldRead.equals(read)) || 388 (flag != null && !oldFlag.equals(flag))) { 389 changes.add(new ServerChange(id, read, flag)); 390 } 391 } 392 393 /* (non-Javadoc) 394 * @see com.android.exchange.adapter.EasContentParser#commandsParser() 395 */ 396 @Override 397 public void commandsParser() throws IOException { 398 ArrayList<Message> newEmails = new ArrayList<Message>(); 399 ArrayList<Long> deletedEmails = new ArrayList<Long>(); 400 ArrayList<ServerChange> changedEmails = new ArrayList<ServerChange>(); 401 int notifyCount = 0; 402 403 while (nextTag(Tags.SYNC_COMMANDS) != END) { 404 if (tag == Tags.SYNC_ADD) { 405 addParser(newEmails); 406 mService.mChangeCount++; 407 } else if (tag == Tags.SYNC_DELETE) { 408 deleteParser(deletedEmails); 409 mService.mChangeCount++; 410 } else if (tag == Tags.SYNC_CHANGE) { 411 changeParser(changedEmails); 412 mService.mChangeCount++; 413 } else 414 skipTag(); 415 } 416 417 // Use a batch operation to handle the changes 418 // TODO New mail notifications? Who looks for these? 419 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 420 for (Message msg: newEmails) { 421 if (!msg.mFlagRead) { 422 notifyCount++; 423 } 424 msg.addSaveOps(ops); 425 } 426 for (Long id : deletedEmails) { 427 ops.add(ContentProviderOperation.newDelete( 428 ContentUris.withAppendedId(Message.CONTENT_URI, id)).build()); 429 } 430 if (!changedEmails.isEmpty()) { 431 // Server wins in a conflict... 432 for (ServerChange change : changedEmails) { 433 ContentValues cv = new ContentValues(); 434 if (change.read != null) { 435 cv.put(MessageColumns.FLAG_READ, change.read); 436 } 437 if (change.flag != null) { 438 cv.put(MessageColumns.FLAG_FAVORITE, change.flag); 439 } 440 ops.add(ContentProviderOperation.newUpdate( 441 ContentUris.withAppendedId(Message.CONTENT_URI, change.id)) 442 .withValues(cv) 443 .build()); 444 } 445 } 446 ops.add(ContentProviderOperation.newUpdate( 447 ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId)).withValues( 448 mMailbox.toContentValues()).build()); 449 450 addCleanupOps(ops); 451 452 // No commits if we're stopped 453 synchronized (mService.getSynchronizer()) { 454 if (mService.isStopped()) return; 455 try { 456 mService.mContext.getContentResolver() 457 .applyBatch(EmailProvider.EMAIL_AUTHORITY, ops); 458 mService.userLog(mMailbox.mDisplayName + 459 " SyncKey saved as: " + mMailbox.mSyncKey); 460 } catch (RemoteException e) { 461 // There is nothing to be done here; fail by returning null 462 } catch (OperationApplicationException e) { 463 // There is nothing to be done here; fail by returning null 464 } 465 } 466 467 // TODO Remove this temporary notification code 468 if (notifyCount > 0) { 469 NotificationManager notifMgr = 470 (NotificationManager)mContext.getSystemService(Context.NOTIFICATION_SERVICE); 471 Notification notif = new Notification(R.drawable.stat_notify_email_generic, 472 mContext.getString(R.string.notification_new_title), 473 System.currentTimeMillis()); 474 Intent i = MessageList.actionHandleAccountIntent(mContext, mAccount.mId, 475 Mailbox.TYPE_INBOX); 476 PendingIntent pi = PendingIntent.getActivity(mContext, 0, i, 0); 477 notif.setLatestEventInfo(mContext, 478 mContext.getString(R.string.notification_new_title), 479 "You've got new mail!", pi); 480 boolean vibrate = ((mAccount.getFlags() & EmailContent.Account.FLAGS_VIBRATE) != 0); 481 String ringtone = mAccount.getRingtone(); 482 notif.defaults = Notification.DEFAULT_LIGHTS; 483 notif.sound = TextUtils.isEmpty(ringtone) ? null : Uri.parse(ringtone); 484 if (vibrate) { 485 notif.defaults |= Notification.DEFAULT_VIBRATE; 486 } 487 notifMgr.notify(1, notif); 488 } 489 } 490 } 491 492 @Override 493 public String getCollectionName() { 494 return "Email"; 495 } 496 497 private void addCleanupOps(ArrayList<ContentProviderOperation> ops) { 498 // If we've sent local deletions, clear out the deleted table 499 for (Long id: mDeletedIdList) { 500 ops.add(ContentProviderOperation.newDelete( 501 ContentUris.withAppendedId(Message.DELETED_CONTENT_URI, id)).build()); 502 } 503 // And same with the updates 504 for (Long id: mUpdatedIdList) { 505 ops.add(ContentProviderOperation.newDelete( 506 ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI, id)).build()); 507 } 508 } 509 510 @Override 511 public void cleanup(EasSyncService service) { 512 if (!mDeletedIdList.isEmpty() || !mUpdatedIdList.isEmpty()) { 513 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 514 addCleanupOps(ops); 515 try { 516 service.mContext.getContentResolver() 517 .applyBatch(EmailProvider.EMAIL_AUTHORITY, ops); 518 } catch (RemoteException e) { 519 // There is nothing to be done here; fail by returning null 520 } catch (OperationApplicationException e) { 521 // There is nothing to be done here; fail by returning null 522 } 523 } 524 } 525 526 @Override 527 public boolean sendLocalChanges(Serializer s, EasSyncService service) throws IOException { 528 Context context = service.mContext; 529 ContentResolver cr = context.getContentResolver(); 530 531 // Find any of our deleted items 532 Cursor c = cr.query(Message.DELETED_CONTENT_URI, Message.LIST_PROJECTION, 533 MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null); 534 boolean first = true; 535 // We keep track of the list of deleted item id's so that we can remove them from the 536 // deleted table after the server receives our command 537 mDeletedIdList.clear(); 538 try { 539 while (c.moveToNext()) { 540 if (first) { 541 s.start(Tags.SYNC_COMMANDS); 542 first = false; 543 } 544 // Send the command to delete this message 545 s.start(Tags.SYNC_DELETE) 546 .data(Tags.SYNC_SERVER_ID, c.getString(Message.LIST_SERVER_ID_COLUMN)) 547 .end(); // SYNC_DELETE 548 mDeletedIdList.add(c.getLong(Message.LIST_ID_COLUMN)); 549 } 550 } finally { 551 c.close(); 552 } 553 554 // Find our trash mailbox, since deletions will have been moved there... 555 long trashMailboxId = 556 Mailbox.findMailboxOfType(context, mMailbox.mAccountKey, Mailbox.TYPE_TRASH); 557 558 // Do the same now for updated items 559 c = cr.query(Message.UPDATED_CONTENT_URI, Message.LIST_PROJECTION, 560 MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null); 561 562 // We keep track of the list of updated item id's as we did above with deleted items 563 mUpdatedIdList.clear(); 564 try { 565 while (c.moveToNext()) { 566 long id = c.getLong(Message.LIST_ID_COLUMN); 567 // Say we've handled this update 568 mUpdatedIdList.add(id); 569 // We have the id of the changed item. But first, we have to find out its current 570 // state, since the updated table saves the opriginal state 571 Cursor currentCursor = cr.query(ContentUris.withAppendedId(Message.CONTENT_URI, id), 572 UPDATES_PROJECTION, null, null, null); 573 try { 574 // If this item no longer exists (shouldn't be possible), just move along 575 if (!currentCursor.moveToFirst()) { 576 continue; 577 } 578 579 // If the message is now in the trash folder, it has been deleted by the user 580 if (currentCursor.getLong(UPDATES_MAILBOX_KEY_COLUMN) == trashMailboxId) { 581 if (first) { 582 s.start(Tags.SYNC_COMMANDS); 583 first = false; 584 } 585 // Send the command to delete this message 586 s.start(Tags.SYNC_DELETE) 587 .data(Tags.SYNC_SERVER_ID, currentCursor.getString(UPDATES_SERVER_ID_COLUMN)) 588 .end(); // SYNC_DELETE 589 continue; 590 } 591 592 int read = currentCursor.getInt(UPDATES_READ_COLUMN); 593 if (read == c.getInt(Message.LIST_READ_COLUMN)) { 594 // The read state hasn't really changed, so move on... 595 continue; 596 } 597 if (first) { 598 s.start(Tags.SYNC_COMMANDS); 599 first = false; 600 } 601 // Send the change to "read". We'll do "flagged" here eventually as well 602 // TODO Add support for flags here (EAS 12.0 and above) 603 // Or is this not safe?? 604 s.start(Tags.SYNC_CHANGE) 605 .data(Tags.SYNC_SERVER_ID, c.getString(Message.LIST_SERVER_ID_COLUMN)) 606 .start(Tags.SYNC_APPLICATION_DATA) 607 .data(Tags.EMAIL_READ, Integer.toString(read)) 608 .end().end(); // SYNC_APPLICATION_DATA, SYNC_CHANGE 609 } finally { 610 currentCursor.close(); 611 } 612 } 613 } finally { 614 c.close(); 615 } 616 617 if (!first) { 618 s.end(); // SYNC_COMMANDS 619 } 620 return false; 621 } 622} 623