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