• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
18 package com.android.exchange.adapter;
19 
20 import com.android.email.Utility;
21 import com.android.email.mail.Address;
22 import com.android.email.mail.MeetingInfo;
23 import com.android.email.mail.PackedString;
24 import com.android.email.provider.AttachmentProvider;
25 import com.android.email.provider.EmailContent;
26 import com.android.email.provider.EmailProvider;
27 import com.android.email.provider.EmailContent.Account;
28 import com.android.email.provider.EmailContent.AccountColumns;
29 import com.android.email.provider.EmailContent.Attachment;
30 import com.android.email.provider.EmailContent.Body;
31 import com.android.email.provider.EmailContent.Mailbox;
32 import com.android.email.provider.EmailContent.Message;
33 import com.android.email.provider.EmailContent.MessageColumns;
34 import com.android.email.provider.EmailContent.SyncColumns;
35 import com.android.email.service.MailService;
36 import com.android.exchange.Eas;
37 import com.android.exchange.EasSyncService;
38 import com.android.exchange.utility.CalendarUtilities;
39 
40 import android.content.ContentProviderOperation;
41 import android.content.ContentResolver;
42 import android.content.ContentUris;
43 import android.content.ContentValues;
44 import android.content.OperationApplicationException;
45 import android.database.Cursor;
46 import android.net.Uri;
47 import android.os.RemoteException;
48 import android.webkit.MimeTypeMap;
49 
50 import java.io.IOException;
51 import java.io.InputStream;
52 import java.util.ArrayList;
53 import java.util.Calendar;
54 import java.util.GregorianCalendar;
55 import java.util.TimeZone;
56 
57 /**
58  * Sync adapter for EAS email
59  *
60  */
61 public class EmailSyncAdapter extends AbstractSyncAdapter {
62 
63     private static final int UPDATES_READ_COLUMN = 0;
64     private static final int UPDATES_MAILBOX_KEY_COLUMN = 1;
65     private static final int UPDATES_SERVER_ID_COLUMN = 2;
66     private static final int UPDATES_FLAG_COLUMN = 3;
67     private static final String[] UPDATES_PROJECTION =
68         {MessageColumns.FLAG_READ, MessageColumns.MAILBOX_KEY, SyncColumns.SERVER_ID,
69             MessageColumns.FLAG_FAVORITE};
70 
71     private static final int MESSAGE_ID_SUBJECT_ID_COLUMN = 0;
72     private static final int MESSAGE_ID_SUBJECT_SUBJECT_COLUMN = 1;
73     private static final String[] MESSAGE_ID_SUBJECT_PROJECTION =
74         new String[] { Message.RECORD_ID, MessageColumns.SUBJECT };
75 
76     private static final String WHERE_BODY_SOURCE_MESSAGE_KEY = Body.SOURCE_MESSAGE_KEY + "=?";
77 
78     String[] mBindArguments = new String[2];
79     String[] mBindArgument = new String[1];
80 
81     ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
82     ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
83 
84     // Holds the parser's value for isLooping()
85     boolean mIsLooping = false;
86 
EmailSyncAdapter(Mailbox mailbox, EasSyncService service)87     public EmailSyncAdapter(Mailbox mailbox, EasSyncService service) {
88         super(mailbox, service);
89     }
90 
91     @Override
parse(InputStream is)92     public boolean parse(InputStream is) throws IOException {
93         EasEmailSyncParser p = new EasEmailSyncParser(is, this);
94         boolean res = p.parse();
95         // Hold on to the parser's value for isLooping() to pass back to the service
96         mIsLooping = p.isLooping();
97         return res;
98     }
99 
100     /**
101      * Return the value of isLooping() as returned from the parser
102      */
103     @Override
isLooping()104     public boolean isLooping() {
105         return mIsLooping;
106     }
107 
108     @Override
isSyncable()109     public boolean isSyncable() {
110         return true;
111     }
112 
113     public class EasEmailSyncParser extends AbstractSyncParser {
114 
115         private static final String WHERE_SERVER_ID_AND_MAILBOX_KEY =
116             SyncColumns.SERVER_ID + "=? and " + MessageColumns.MAILBOX_KEY + "=?";
117 
118         private String mMailboxIdAsString;
119 
120         ArrayList<Message> newEmails = new ArrayList<Message>();
121         ArrayList<Long> deletedEmails = new ArrayList<Long>();
122         ArrayList<ServerChange> changedEmails = new ArrayList<ServerChange>();
123 
EasEmailSyncParser(InputStream in, EmailSyncAdapter adapter)124         public EasEmailSyncParser(InputStream in, EmailSyncAdapter adapter) throws IOException {
125             super(in, adapter);
126             mMailboxIdAsString = Long.toString(mMailbox.mId);
127         }
128 
129         @Override
wipe()130         public void wipe() {
131             mContentResolver.delete(Message.CONTENT_URI,
132                     Message.MAILBOX_KEY + "=" + mMailbox.mId, null);
133             mContentResolver.delete(Message.DELETED_CONTENT_URI,
134                     Message.MAILBOX_KEY + "=" + mMailbox.mId, null);
135             mContentResolver.delete(Message.UPDATED_CONTENT_URI,
136                     Message.MAILBOX_KEY + "=" + mMailbox.mId, null);
137         }
138 
addData(Message msg)139         public void addData (Message msg) throws IOException {
140             ArrayList<Attachment> atts = new ArrayList<Attachment>();
141 
142             while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
143                 switch (tag) {
144                     case Tags.EMAIL_ATTACHMENTS:
145                     case Tags.BASE_ATTACHMENTS: // BASE_ATTACHMENTS is used in EAS 12.0 and up
146                         attachmentsParser(atts, msg);
147                         break;
148                     case Tags.EMAIL_TO:
149                         msg.mTo = Address.pack(Address.parse(getValue()));
150                         break;
151                     case Tags.EMAIL_FROM:
152                         Address[] froms = Address.parse(getValue());
153                         if (froms != null && froms.length > 0) {
154                           msg.mDisplayName = froms[0].toFriendly();
155                         }
156                         msg.mFrom = Address.pack(froms);
157                         break;
158                     case Tags.EMAIL_CC:
159                         msg.mCc = Address.pack(Address.parse(getValue()));
160                         break;
161                     case Tags.EMAIL_REPLY_TO:
162                         msg.mReplyTo = Address.pack(Address.parse(getValue()));
163                         break;
164                     case Tags.EMAIL_DATE_RECEIVED:
165                         msg.mTimeStamp = Utility.parseEmailDateTimeToMillis(getValue());
166                         break;
167                     case Tags.EMAIL_SUBJECT:
168                         msg.mSubject = getValue();
169                         break;
170                     case Tags.EMAIL_READ:
171                         msg.mFlagRead = getValueInt() == 1;
172                         break;
173                     case Tags.BASE_BODY:
174                         bodyParser(msg);
175                         break;
176                     case Tags.EMAIL_FLAG:
177                         msg.mFlagFavorite = flagParser();
178                         break;
179                     case Tags.EMAIL_BODY:
180                         String text = getValue();
181                         msg.mText = text;
182                         break;
183                     case Tags.EMAIL_MESSAGE_CLASS:
184                         String messageClass = getValue();
185                         if (messageClass.equals("IPM.Schedule.Meeting.Request")) {
186                             msg.mFlags |= Message.FLAG_INCOMING_MEETING_INVITE;
187                         } else if (messageClass.equals("IPM.Schedule.Meeting.Canceled")) {
188                             msg.mFlags |= Message.FLAG_INCOMING_MEETING_CANCEL;
189                         }
190                         break;
191                     case Tags.EMAIL_MEETING_REQUEST:
192                         meetingRequestParser(msg);
193                         break;
194                     default:
195                         skipTag();
196                 }
197             }
198 
199             if (atts.size() > 0) {
200                 msg.mAttachments = atts;
201             }
202         }
203 
204         /**
205          * Set up the meetingInfo field in the message with various pieces of information gleaned
206          * from MeetingRequest tags.  This information will be used later to generate an appropriate
207          * reply email if the user chooses to respond
208          * @param msg the Message being built
209          * @throws IOException
210          */
meetingRequestParser(Message msg)211         private void meetingRequestParser(Message msg) throws IOException {
212             PackedString.Builder packedString = new PackedString.Builder();
213             while (nextTag(Tags.EMAIL_MEETING_REQUEST) != END) {
214                 switch (tag) {
215                     case Tags.EMAIL_DTSTAMP:
216                         packedString.put(MeetingInfo.MEETING_DTSTAMP, getValue());
217                         break;
218                     case Tags.EMAIL_START_TIME:
219                         packedString.put(MeetingInfo.MEETING_DTSTART, getValue());
220                         break;
221                     case Tags.EMAIL_END_TIME:
222                         packedString.put(MeetingInfo.MEETING_DTEND, getValue());
223                         break;
224                     case Tags.EMAIL_ORGANIZER:
225                         packedString.put(MeetingInfo.MEETING_ORGANIZER_EMAIL, getValue());
226                         break;
227                     case Tags.EMAIL_LOCATION:
228                         packedString.put(MeetingInfo.MEETING_LOCATION, getValue());
229                         break;
230                     case Tags.EMAIL_GLOBAL_OBJID:
231                         packedString.put(MeetingInfo.MEETING_UID,
232                                 CalendarUtilities.getUidFromGlobalObjId(getValue()));
233                         break;
234                     case Tags.EMAIL_CATEGORIES:
235                         nullParser();
236                         break;
237                     case Tags.EMAIL_RECURRENCES:
238                         recurrencesParser();
239                         break;
240                     default:
241                         skipTag();
242                 }
243             }
244             if (msg.mSubject != null) {
245                 packedString.put(MeetingInfo.MEETING_TITLE, msg.mSubject);
246             }
247             msg.mMeetingInfo = packedString.toString();
248         }
249 
nullParser()250         private void nullParser() throws IOException {
251             while (nextTag(Tags.EMAIL_CATEGORIES) != END) {
252                 skipTag();
253             }
254         }
255 
recurrencesParser()256         private void recurrencesParser() throws IOException {
257             while (nextTag(Tags.EMAIL_RECURRENCES) != END) {
258                 switch (tag) {
259                     case Tags.EMAIL_RECURRENCE:
260                         nullParser();
261                         break;
262                     default:
263                         skipTag();
264                 }
265             }
266         }
267 
addParser(ArrayList<Message> emails)268         private void addParser(ArrayList<Message> emails) throws IOException {
269             Message msg = new Message();
270             msg.mAccountKey = mAccount.mId;
271             msg.mMailboxKey = mMailbox.mId;
272             msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE;
273 
274             while (nextTag(Tags.SYNC_ADD) != END) {
275                 switch (tag) {
276                     case Tags.SYNC_SERVER_ID:
277                         msg.mServerId = getValue();
278                         break;
279                     case Tags.SYNC_APPLICATION_DATA:
280                         addData(msg);
281                         break;
282                     default:
283                         skipTag();
284                 }
285             }
286             emails.add(msg);
287         }
288 
289         // For now, we only care about the "active" state
flagParser()290         private Boolean flagParser() throws IOException {
291             Boolean state = false;
292             while (nextTag(Tags.EMAIL_FLAG) != END) {
293                 switch (tag) {
294                     case Tags.EMAIL_FLAG_STATUS:
295                         state = getValueInt() == 2;
296                         break;
297                     default:
298                         skipTag();
299                 }
300             }
301             return state;
302         }
303 
bodyParser(Message msg)304         private void bodyParser(Message msg) throws IOException {
305             String bodyType = Eas.BODY_PREFERENCE_TEXT;
306             String body = "";
307             while (nextTag(Tags.EMAIL_BODY) != END) {
308                 switch (tag) {
309                     case Tags.BASE_TYPE:
310                         bodyType = getValue();
311                         break;
312                     case Tags.BASE_DATA:
313                         body = getValue();
314                         break;
315                     default:
316                         skipTag();
317                 }
318             }
319             // We always ask for TEXT or HTML; there's no third option
320             if (bodyType.equals(Eas.BODY_PREFERENCE_HTML)) {
321                 msg.mHtml = body;
322             } else {
323                 msg.mText = body;
324             }
325         }
326 
attachmentsParser(ArrayList<Attachment> atts, Message msg)327         private void attachmentsParser(ArrayList<Attachment> atts, Message msg) throws IOException {
328             while (nextTag(Tags.EMAIL_ATTACHMENTS) != END) {
329                 switch (tag) {
330                     case Tags.EMAIL_ATTACHMENT:
331                     case Tags.BASE_ATTACHMENT:  // BASE_ATTACHMENT is used in EAS 12.0 and up
332                         attachmentParser(atts, msg);
333                         break;
334                     default:
335                         skipTag();
336                 }
337             }
338         }
339 
attachmentParser(ArrayList<Attachment> atts, Message msg)340         private void attachmentParser(ArrayList<Attachment> atts, Message msg) throws IOException {
341             String fileName = null;
342             String length = null;
343             String location = null;
344 
345             while (nextTag(Tags.EMAIL_ATTACHMENT) != END) {
346                 switch (tag) {
347                     // We handle both EAS 2.5 and 12.0+ attachments here
348                     case Tags.EMAIL_DISPLAY_NAME:
349                     case Tags.BASE_DISPLAY_NAME:
350                         fileName = getValue();
351                         break;
352                     case Tags.EMAIL_ATT_NAME:
353                     case Tags.BASE_FILE_REFERENCE:
354                         location = getValue();
355                         break;
356                     case Tags.EMAIL_ATT_SIZE:
357                     case Tags.BASE_ESTIMATED_DATA_SIZE:
358                         length = getValue();
359                         break;
360                     default:
361                         skipTag();
362                 }
363             }
364 
365             if ((fileName != null) && (length != null) && (location != null)) {
366                 Attachment att = new Attachment();
367                 att.mEncoding = "base64";
368                 att.mSize = Long.parseLong(length);
369                 att.mFileName = fileName;
370                 att.mLocation = location;
371                 att.mMimeType = getMimeTypeFromFileName(fileName);
372                 atts.add(att);
373                 msg.mFlagAttachment = true;
374             }
375         }
376 
377         /**
378          * Try to determine a mime type from a file name, defaulting to application/x, where x
379          * is either the extension or (if none) octet-stream
380          * At the moment, this is somewhat lame, since many file types aren't recognized
381          * @param fileName the file name to ponder
382          * @return
383          */
384         // Note: The MimeTypeMap method currently uses a very limited set of mime types
385         // A bug has been filed against this issue.
getMimeTypeFromFileName(String fileName)386         public String getMimeTypeFromFileName(String fileName) {
387             String mimeType;
388             int lastDot = fileName.lastIndexOf('.');
389             String extension = null;
390             if ((lastDot > 0) && (lastDot < fileName.length() - 1)) {
391                 extension = fileName.substring(lastDot + 1).toLowerCase();
392             }
393             if (extension == null) {
394                 // A reasonable default for now.
395                 mimeType = "application/octet-stream";
396             } else {
397                 mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
398                 if (mimeType == null) {
399                     mimeType = "application/" + extension;
400                 }
401             }
402             return mimeType;
403         }
404 
getServerIdCursor(String serverId, String[] projection)405         private Cursor getServerIdCursor(String serverId, String[] projection) {
406             mBindArguments[0] = serverId;
407             mBindArguments[1] = mMailboxIdAsString;
408             return mContentResolver.query(Message.CONTENT_URI, projection,
409                     WHERE_SERVER_ID_AND_MAILBOX_KEY, mBindArguments, null);
410         }
411 
deleteParser(ArrayList<Long> deletes, int entryTag)412         /*package*/ void deleteParser(ArrayList<Long> deletes, int entryTag) throws IOException {
413             while (nextTag(entryTag) != END) {
414                 switch (tag) {
415                     case Tags.SYNC_SERVER_ID:
416                         String serverId = getValue();
417                         // Find the message in this mailbox with the given serverId
418                         Cursor c = getServerIdCursor(serverId, MESSAGE_ID_SUBJECT_PROJECTION);
419                         try {
420                             if (c.moveToFirst()) {
421                                 deletes.add(c.getLong(MESSAGE_ID_SUBJECT_ID_COLUMN));
422                                 if (Eas.USER_LOG) {
423                                     userLog("Deleting ", serverId + ", "
424                                             + c.getString(MESSAGE_ID_SUBJECT_SUBJECT_COLUMN));
425                                 }
426                             }
427                         } finally {
428                             c.close();
429                         }
430                         break;
431                     default:
432                         skipTag();
433                 }
434             }
435         }
436 
437         class ServerChange {
438             long id;
439             Boolean read;
440             Boolean flag;
441 
ServerChange(long _id, Boolean _read, Boolean _flag)442             ServerChange(long _id, Boolean _read, Boolean _flag) {
443                 id = _id;
444                 read = _read;
445                 flag = _flag;
446             }
447         }
448 
changeParser(ArrayList<ServerChange> changes)449         /*package*/ void changeParser(ArrayList<ServerChange> changes) throws IOException {
450             String serverId = null;
451             Boolean oldRead = false;
452             Boolean oldFlag = false;
453             long id = 0;
454             while (nextTag(Tags.SYNC_CHANGE) != END) {
455                 switch (tag) {
456                     case Tags.SYNC_SERVER_ID:
457                         serverId = getValue();
458                         Cursor c = getServerIdCursor(serverId, Message.LIST_PROJECTION);
459                         try {
460                             if (c.moveToFirst()) {
461                                 userLog("Changing ", serverId);
462                                 oldRead = c.getInt(Message.LIST_READ_COLUMN) == Message.READ;
463                                 oldFlag = c.getInt(Message.LIST_FAVORITE_COLUMN) == 1;
464                                 id = c.getLong(Message.LIST_ID_COLUMN);
465                             }
466                         } finally {
467                             c.close();
468                         }
469                         break;
470                     case Tags.SYNC_APPLICATION_DATA:
471                         changeApplicationDataParser(changes, oldRead, oldFlag, id);
472                         break;
473                     default:
474                         skipTag();
475                 }
476             }
477         }
478 
changeApplicationDataParser(ArrayList<ServerChange> changes, Boolean oldRead, Boolean oldFlag, long id)479         private void changeApplicationDataParser(ArrayList<ServerChange> changes, Boolean oldRead,
480                 Boolean oldFlag, long id) throws IOException {
481             Boolean read = null;
482             Boolean flag = null;
483             while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
484                 switch (tag) {
485                     case Tags.EMAIL_READ:
486                         read = getValueInt() == 1;
487                         break;
488                     case Tags.EMAIL_FLAG:
489                         flag = flagParser();
490                         break;
491                     default:
492                         skipTag();
493                 }
494             }
495             if (((read != null) && !oldRead.equals(read)) ||
496                     ((flag != null) && !oldFlag.equals(flag))) {
497                 changes.add(new ServerChange(id, read, flag));
498             }
499         }
500 
501         /* (non-Javadoc)
502          * @see com.android.exchange.adapter.EasContentParser#commandsParser()
503          */
504         @Override
commandsParser()505         public void commandsParser() throws IOException {
506             while (nextTag(Tags.SYNC_COMMANDS) != END) {
507                 if (tag == Tags.SYNC_ADD) {
508                     addParser(newEmails);
509                     incrementChangeCount();
510                 } else if (tag == Tags.SYNC_DELETE || tag == Tags.SYNC_SOFT_DELETE) {
511                     deleteParser(deletedEmails, tag);
512                     incrementChangeCount();
513                 } else if (tag == Tags.SYNC_CHANGE) {
514                     changeParser(changedEmails);
515                     incrementChangeCount();
516                 } else
517                     skipTag();
518             }
519         }
520 
521         @Override
responsesParser()522         public void responsesParser() {
523         }
524 
525         @Override
commit()526         public void commit() {
527             int notifyCount = 0;
528 
529             // Use a batch operation to handle the changes
530             // TODO New mail notifications?  Who looks for these?
531             ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
532             for (Message msg: newEmails) {
533                 if (!msg.mFlagRead) {
534                     notifyCount++;
535                 }
536                 msg.addSaveOps(ops);
537             }
538             for (Long id : deletedEmails) {
539                 ops.add(ContentProviderOperation.newDelete(
540                         ContentUris.withAppendedId(Message.CONTENT_URI, id)).build());
541                 AttachmentProvider.deleteAllAttachmentFiles(mContext, mAccount.mId, id);
542             }
543             if (!changedEmails.isEmpty()) {
544                 // Server wins in a conflict...
545                 for (ServerChange change : changedEmails) {
546                      ContentValues cv = new ContentValues();
547                     if (change.read != null) {
548                         cv.put(MessageColumns.FLAG_READ, change.read);
549                     }
550                     if (change.flag != null) {
551                         cv.put(MessageColumns.FLAG_FAVORITE, change.flag);
552                     }
553                     ops.add(ContentProviderOperation.newUpdate(
554                             ContentUris.withAppendedId(Message.CONTENT_URI, change.id))
555                                 .withValues(cv)
556                                 .build());
557                 }
558             }
559 
560             // We only want to update the sync key here
561             ContentValues mailboxValues = new ContentValues();
562             mailboxValues.put(Mailbox.SYNC_KEY, mMailbox.mSyncKey);
563             ops.add(ContentProviderOperation.newUpdate(
564                     ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId))
565                         .withValues(mailboxValues).build());
566 
567             addCleanupOps(ops);
568 
569             // No commits if we're stopped
570             synchronized (mService.getSynchronizer()) {
571                 if (mService.isStopped()) return;
572                 try {
573                     mContentResolver.applyBatch(EmailProvider.EMAIL_AUTHORITY, ops);
574                     userLog(mMailbox.mDisplayName, " SyncKey saved as: ", mMailbox.mSyncKey);
575                 } catch (RemoteException e) {
576                     // There is nothing to be done here; fail by returning null
577                 } catch (OperationApplicationException e) {
578                     // There is nothing to be done here; fail by returning null
579                 }
580             }
581 
582             if (notifyCount > 0) {
583                 // Use the new atomic add URI in EmailProvider
584                 // We could add this to the operations being done, but it's not strictly
585                 // speaking necessary, as the previous batch preserves the integrity of the
586                 // database, whereas this is purely for notification purposes, and is itself atomic
587                 ContentValues cv = new ContentValues();
588                 cv.put(EmailContent.FIELD_COLUMN_NAME, AccountColumns.NEW_MESSAGE_COUNT);
589                 cv.put(EmailContent.ADD_COLUMN_NAME, notifyCount);
590                 Uri uri = ContentUris.withAppendedId(Account.ADD_TO_FIELD_URI, mAccount.mId);
591                 mContentResolver.update(uri, cv, null, null);
592                 MailService.actionNotifyNewMessages(mContext, mAccount.mId);
593             }
594         }
595     }
596 
597     @Override
getCollectionName()598     public String getCollectionName() {
599         return "Email";
600     }
601 
addCleanupOps(ArrayList<ContentProviderOperation> ops)602     private void addCleanupOps(ArrayList<ContentProviderOperation> ops) {
603         // If we've sent local deletions, clear out the deleted table
604         for (Long id: mDeletedIdList) {
605             ops.add(ContentProviderOperation.newDelete(
606                     ContentUris.withAppendedId(Message.DELETED_CONTENT_URI, id)).build());
607         }
608         // And same with the updates
609         for (Long id: mUpdatedIdList) {
610             ops.add(ContentProviderOperation.newDelete(
611                     ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI, id)).build());
612         }
613     }
614 
615     @Override
cleanup()616     public void cleanup() {
617         if (!mDeletedIdList.isEmpty() || !mUpdatedIdList.isEmpty()) {
618             ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
619             addCleanupOps(ops);
620             try {
621                 mContext.getContentResolver()
622                     .applyBatch(EmailProvider.EMAIL_AUTHORITY, ops);
623             } catch (RemoteException e) {
624                 // There is nothing to be done here; fail by returning null
625             } catch (OperationApplicationException e) {
626                 // There is nothing to be done here; fail by returning null
627             }
628         }
629     }
630 
formatTwo(int num)631     private String formatTwo(int num) {
632         if (num < 10) {
633             return "0" + (char)('0' + num);
634         } else
635             return Integer.toString(num);
636     }
637 
638     /**
639      * Create date/time in RFC8601 format.  Oddly enough, for calendar date/time, Microsoft uses
640      * a different format that excludes the punctuation (this is why I'm not putting this in a
641      * parent class)
642      */
formatDateTime(Calendar calendar)643     public String formatDateTime(Calendar calendar) {
644         StringBuilder sb = new StringBuilder();
645         //YYYY-MM-DDTHH:MM:SS.MSSZ
646         sb.append(calendar.get(Calendar.YEAR));
647         sb.append('-');
648         sb.append(formatTwo(calendar.get(Calendar.MONTH) + 1));
649         sb.append('-');
650         sb.append(formatTwo(calendar.get(Calendar.DAY_OF_MONTH)));
651         sb.append('T');
652         sb.append(formatTwo(calendar.get(Calendar.HOUR_OF_DAY)));
653         sb.append(':');
654         sb.append(formatTwo(calendar.get(Calendar.MINUTE)));
655         sb.append(':');
656         sb.append(formatTwo(calendar.get(Calendar.SECOND)));
657         sb.append(".000Z");
658         return sb.toString();
659     }
660 
661     /**
662      * Note that messages in the deleted database preserve the message's unique id; therefore, we
663      * can utilize this id to find references to the message.  The only reference situation at this
664      * point is in the Body table; it is when sending messages via SmartForward and SmartReply
665      */
messageReferenced(ContentResolver cr, long id)666     private boolean messageReferenced(ContentResolver cr, long id) {
667         mBindArgument[0] = Long.toString(id);
668         // See if this id is referenced in a body
669         Cursor c = cr.query(Body.CONTENT_URI, Body.ID_PROJECTION, WHERE_BODY_SOURCE_MESSAGE_KEY,
670                 mBindArgument, null);
671         try {
672             return c.moveToFirst();
673         } finally {
674             c.close();
675         }
676     }
677 
678     /*private*/ /**
679      * Serialize commands to delete items from the server; as we find items to delete, add their
680      * id's to the deletedId's array
681      *
682      * @param s the Serializer we're using to create post data
683      * @param deletedIds ids whose deletions are being sent to the server
684      * @param first whether or not this is the first command being sent
685      * @return true if SYNC_COMMANDS hasn't been sent (false otherwise)
686      * @throws IOException
687      */
sendDeletedItems(Serializer s, ArrayList<Long> deletedIds, boolean first)688     boolean sendDeletedItems(Serializer s, ArrayList<Long> deletedIds, boolean first)
689             throws IOException {
690         ContentResolver cr = mContext.getContentResolver();
691 
692         // Find any of our deleted items
693         Cursor c = cr.query(Message.DELETED_CONTENT_URI, Message.LIST_PROJECTION,
694                 MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null);
695         // We keep track of the list of deleted item id's so that we can remove them from the
696         // deleted table after the server receives our command
697         deletedIds.clear();
698         try {
699             while (c.moveToNext()) {
700                 String serverId = c.getString(Message.LIST_SERVER_ID_COLUMN);
701                 // Keep going if there's no serverId
702                 if (serverId == null) {
703                     continue;
704                 // Also check if this message is referenced elsewhere
705                 } else if (messageReferenced(cr, c.getLong(Message.CONTENT_ID_COLUMN))) {
706                     userLog("Postponing deletion of referenced message: ", serverId);
707                     continue;
708                 } else if (first) {
709                     s.start(Tags.SYNC_COMMANDS);
710                     first = false;
711                 }
712                 // Send the command to delete this message
713                 s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
714                 deletedIds.add(c.getLong(Message.LIST_ID_COLUMN));
715             }
716         } finally {
717             c.close();
718         }
719 
720        return first;
721     }
722 
723     @Override
sendLocalChanges(Serializer s)724     public boolean sendLocalChanges(Serializer s) throws IOException {
725         ContentResolver cr = mContext.getContentResolver();
726 
727         // Never upsync from these folders
728         if (mMailbox.mType == Mailbox.TYPE_DRAFTS || mMailbox.mType == Mailbox.TYPE_OUTBOX) {
729             return false;
730         }
731 
732         // This code is split out for unit testing purposes
733         boolean firstCommand = sendDeletedItems(s, mDeletedIdList, true);
734 
735         // Find our trash mailbox, since deletions will have been moved there...
736         long trashMailboxId =
737             Mailbox.findMailboxOfType(mContext, mMailbox.mAccountKey, Mailbox.TYPE_TRASH);
738 
739         // Do the same now for updated items
740         Cursor c = cr.query(Message.UPDATED_CONTENT_URI, Message.LIST_PROJECTION,
741                 MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null);
742 
743         // We keep track of the list of updated item id's as we did above with deleted items
744         mUpdatedIdList.clear();
745         try {
746             while (c.moveToNext()) {
747                 long id = c.getLong(Message.LIST_ID_COLUMN);
748                 // Say we've handled this update
749                 mUpdatedIdList.add(id);
750                 // We have the id of the changed item.  But first, we have to find out its current
751                 // state, since the updated table saves the opriginal state
752                 Cursor currentCursor = cr.query(ContentUris.withAppendedId(Message.CONTENT_URI, id),
753                         UPDATES_PROJECTION, null, null, null);
754                 try {
755                     // If this item no longer exists (shouldn't be possible), just move along
756                     if (!currentCursor.moveToFirst()) {
757                          continue;
758                     }
759                     // Keep going if there's no serverId
760                     String serverId = currentCursor.getString(UPDATES_SERVER_ID_COLUMN);
761                     if (serverId == null) {
762                         continue;
763                     }
764                     // If the message is now in the trash folder, it has been deleted by the user
765                     if (currentCursor.getLong(UPDATES_MAILBOX_KEY_COLUMN) == trashMailboxId) {
766                          if (firstCommand) {
767                             s.start(Tags.SYNC_COMMANDS);
768                             firstCommand = false;
769                         }
770                         // Send the command to delete this message
771                         s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
772                         continue;
773                     }
774 
775                     boolean flagChange = false;
776                     boolean readChange = false;
777 
778                     int flag = 0;
779 
780                     // We can only send flag changes to the server in 12.0 or later
781                     if (mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
782                         flag = currentCursor.getInt(UPDATES_FLAG_COLUMN);
783                         if (flag != c.getInt(Message.LIST_FAVORITE_COLUMN)) {
784                             flagChange = true;
785                         }
786                     }
787 
788                     int read = currentCursor.getInt(UPDATES_READ_COLUMN);
789                     if (read != c.getInt(Message.LIST_READ_COLUMN)) {
790                         readChange = true;
791                     }
792 
793                     if (!flagChange && !readChange) {
794                         // In this case, we've got nothing to send to the server
795                         continue;
796                     }
797 
798                     if (firstCommand) {
799                         s.start(Tags.SYNC_COMMANDS);
800                         firstCommand = false;
801                     }
802                     // Send the change to "read" and "favorite" (flagged)
803                     s.start(Tags.SYNC_CHANGE)
804                         .data(Tags.SYNC_SERVER_ID, c.getString(Message.LIST_SERVER_ID_COLUMN))
805                         .start(Tags.SYNC_APPLICATION_DATA);
806                     if (readChange) {
807                         s.data(Tags.EMAIL_READ, Integer.toString(read));
808                     }
809                     // "Flag" is a relatively complex concept in EAS 12.0 and above.  It is not only
810                     // the boolean "favorite" that we think of in Gmail, but it also represents a
811                     // follow up action, which can include a subject, start and due dates, and even
812                     // recurrences.  We don't support any of this as yet, but EAS 12.0 and higher
813                     // require that a flag contain a status, a type, and four date fields, two each
814                     // for start date and end (due) date.
815                     if (flagChange) {
816                         if (flag != 0) {
817                             // Status 2 = set flag
818                             s.start(Tags.EMAIL_FLAG).data(Tags.EMAIL_FLAG_STATUS, "2");
819                             // "FollowUp" is the standard type
820                             s.data(Tags.EMAIL_FLAG_TYPE, "FollowUp");
821                             long now = System.currentTimeMillis();
822                             Calendar calendar =
823                                 GregorianCalendar.getInstance(TimeZone.getTimeZone("GMT"));
824                             calendar.setTimeInMillis(now);
825                             // Flags are required to have a start date and end date (duplicated)
826                             // First, we'll set the current date/time in GMT as the start time
827                             String utc = formatDateTime(calendar);
828                             s.data(Tags.TASK_START_DATE, utc).data(Tags.TASK_UTC_START_DATE, utc);
829                             // And then we'll use one week from today for completion date
830                             calendar.setTimeInMillis(now + 1*WEEKS);
831                             utc = formatDateTime(calendar);
832                             s.data(Tags.TASK_DUE_DATE, utc).data(Tags.TASK_UTC_DUE_DATE, utc);
833                             s.end();
834                         } else {
835                             s.tag(Tags.EMAIL_FLAG);
836                         }
837                     }
838                     s.end().end(); // SYNC_APPLICATION_DATA, SYNC_CHANGE
839                 } finally {
840                     currentCursor.close();
841                 }
842             }
843         } finally {
844             c.close();
845         }
846 
847         if (!firstCommand) {
848             s.end(); // SYNC_COMMANDS
849         }
850         return false;
851     }
852 }
853