EasSyncService.java revision 8047ef058e41c164c2c8ab230ae8d123f042c167
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;
19
20import com.android.email.R;
21import com.android.email.activity.AccountFolderList;
22import com.android.email.mail.AuthenticationFailedException;
23import com.android.email.mail.MessagingException;
24import com.android.email.provider.EmailContent.Account;
25import com.android.email.provider.EmailContent.AccountColumns;
26import com.android.email.provider.EmailContent.Attachment;
27import com.android.email.provider.EmailContent.AttachmentColumns;
28import com.android.email.provider.EmailContent.HostAuth;
29import com.android.email.provider.EmailContent.Mailbox;
30import com.android.email.provider.EmailContent.MailboxColumns;
31import com.android.email.provider.EmailContent.Message;
32import com.android.exchange.adapter.AbstractSyncAdapter;
33import com.android.exchange.adapter.ContactsSyncAdapter;
34import com.android.exchange.adapter.EmailSyncAdapter;
35import com.android.exchange.adapter.FolderSyncParser;
36import com.android.exchange.adapter.PingParser;
37import com.android.exchange.adapter.Serializer;
38import com.android.exchange.adapter.Tags;
39import com.android.exchange.adapter.Parser.EasParserException;
40import com.android.exchange.utility.Base64;
41
42import org.apache.http.Header;
43import org.apache.http.HttpEntity;
44import org.apache.http.HttpResponse;
45import org.apache.http.client.HttpClient;
46import org.apache.http.client.methods.HttpOptions;
47import org.apache.http.client.methods.HttpPost;
48import org.apache.http.client.methods.HttpRequestBase;
49import org.apache.http.entity.ByteArrayEntity;
50import org.apache.http.impl.client.DefaultHttpClient;
51import org.apache.http.params.BasicHttpParams;
52import org.apache.http.params.HttpConnectionParams;
53import org.apache.http.params.HttpParams;
54
55import android.app.Notification;
56import android.app.NotificationManager;
57import android.app.PendingIntent;
58import android.content.ContentResolver;
59import android.content.ContentUris;
60import android.content.ContentValues;
61import android.content.Context;
62import android.content.Intent;
63import android.database.Cursor;
64import android.os.RemoteException;
65
66import java.io.ByteArrayInputStream;
67import java.io.File;
68import java.io.FileOutputStream;
69import java.io.IOException;
70import java.io.InputStream;
71import java.net.HttpURLConnection;
72import java.net.URI;
73import java.net.URLEncoder;
74import java.util.ArrayList;
75import java.util.HashMap;
76
77public class EasSyncService extends AbstractSyncService {
78
79    private static final String EMAIL_WINDOW_SIZE = "5";
80    public static final String PIM_WINDOW_SIZE = "20";
81    private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID =
82        MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SERVER_ID + "=?";
83    private static final String WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING =
84        MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL +
85        '=' + Account.CHECK_INTERVAL_PING;
86    private static final String AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX = " AND " +
87        MailboxColumns.SYNC_INTERVAL + " IN (" + Account.CHECK_INTERVAL_PING +
88        ',' + Account.CHECK_INTERVAL_PUSH + ") AND " + MailboxColumns.TYPE + "!=\"" +
89        Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + '\"';
90    private static final String WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX =
91        MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL +
92        '=' + Account.CHECK_INTERVAL_PUSH_HOLD;
93
94    static private final int CHUNK_SIZE = 16*1024;
95
96    static private final String PING_COMMAND = "Ping";
97    static private final int COMMAND_TIMEOUT = 20*SECS;
98    static private final int PING_COMMAND_TIMEOUT = 20*MINS;
99
100    // Reasonable default
101    String mProtocolVersion = "2.5";
102    public Double mProtocolVersionDouble;
103    private String mDeviceId = null;
104    private String mDeviceType = "Android";
105    AbstractSyncAdapter mTarget;
106    String mAuthString = null;
107    String mCmdString = null;
108    public String mHostAddress;
109    public String mUserName;
110    public String mPassword;
111    String mDomain = null;
112    boolean mSentCommands;
113    boolean mIsIdle = false;
114    boolean mSsl = true;
115    public Context mContext;
116    public ContentResolver mContentResolver;
117    String[] mBindArguments = new String[2];
118    InputStream mPendingPartInputStream = null;
119    private boolean mTriedReloadFolderList = false;
120
121    public EasSyncService(Context _context, Mailbox _mailbox) {
122        super(_context, _mailbox);
123        mContext = _context;
124        mContentResolver = _context.getContentResolver();
125        HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv);
126        mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0;
127    }
128
129    private EasSyncService(String prefix) {
130        super(prefix);
131    }
132
133    public EasSyncService() {
134        this("EAS Validation");
135    }
136
137    @Override
138    public void ping() {
139        userLog("We've been pinged!");
140        Object synchronizer = getSynchronizer();
141        synchronized (synchronizer) {
142            synchronizer.notify();
143        }
144    }
145
146    @Override
147    public void stop() {
148        mStop = true;
149     }
150
151    @Override
152    public int getSyncStatus() {
153        return 0;
154    }
155
156    private boolean isAuthError(int code) {
157        return (code == HttpURLConnection.HTTP_UNAUTHORIZED || code == HttpURLConnection.HTTP_FORBIDDEN
158                || code == HttpURLConnection.HTTP_INTERNAL_ERROR);
159    }
160
161    /* (non-Javadoc)
162     * @see com.android.exchange.SyncService#validateAccount(java.lang.String, java.lang.String, java.lang.String, int, boolean, android.content.Context)
163     */
164    @Override
165    public void validateAccount(String hostAddress, String userName, String password, int port,
166            boolean ssl, Context context) throws MessagingException {
167        try {
168            userLog("Testing EAS: " + hostAddress + ", " + userName + ", ssl = " + ssl);
169            EasSyncService svc = new EasSyncService("%TestAccount%");
170            svc.mContext = context;
171            svc.mHostAddress = hostAddress;
172            svc.mUserName = userName;
173            svc.mPassword = password;
174            svc.mSsl = ssl;
175            svc.mDeviceId = SyncManager.getDeviceId();
176            HttpResponse resp = svc.sendHttpClientOptions();
177            int code = resp.getStatusLine().getStatusCode();
178            userLog("Validation (OPTIONS) response: " + code);
179            if (code == HttpURLConnection.HTTP_OK) {
180                // No exception means successful validation
181                userLog("Validation successful");
182                return;
183            }
184            if (isAuthError(code)) {
185                userLog("Authentication failed");
186                throw new AuthenticationFailedException("Validation failed");
187            } else {
188                // TODO Need to catch other kinds of errors (e.g. policy) For now, report the code.
189                userLog("Validation failed, reporting I/O error: " + code);
190                throw new MessagingException(MessagingException.IOERROR);
191            }
192        } catch (IOException e) {
193            userLog("IOException caught, reporting I/O error: " + e.getMessage());
194            throw new MessagingException(MessagingException.IOERROR);
195        }
196
197    }
198
199    private void doStatusCallback(long messageId, long attachmentId, int status) {
200        try {
201            SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, status, 0);
202        } catch (RemoteException e) {
203            // No danger if the client is no longer around
204        }
205    }
206
207    private void doProgressCallback(long messageId, long attachmentId, int progress) {
208        try {
209            SyncManager.callback().loadAttachmentStatus(messageId, attachmentId,
210                    EmailServiceStatus.IN_PROGRESS, progress);
211        } catch (RemoteException e) {
212            // No danger if the client is no longer around
213        }
214    }
215
216    public File createUniqueFileInternal(String dir, String filename) {
217        File directory;
218        if (dir == null) {
219            directory = mContext.getFilesDir();
220        } else {
221            directory = new File(dir);
222        }
223        if (!directory.exists()) {
224            directory.mkdirs();
225        }
226        File file = new File(directory, filename);
227        if (!file.exists()) {
228            return file;
229        }
230        // Get the extension of the file, if any.
231        int index = filename.lastIndexOf('.');
232        String name = filename;
233        String extension = "";
234        if (index != -1) {
235            name = filename.substring(0, index);
236            extension = filename.substring(index);
237        }
238        for (int i = 2; i < Integer.MAX_VALUE; i++) {
239            file = new File(directory, name + '-' + i + extension);
240            if (!file.exists()) {
241                return file;
242            }
243        }
244        return null;
245    }
246
247    /**
248     * Loads an attachment, based on the PartRequest passed in.  The PartRequest is basically our
249     * wrapper for Attachment
250     * @param req the part (attachment) to be retrieved
251     * @throws IOException
252     */
253    protected void getAttachment(PartRequest req) throws IOException {
254        Attachment att = req.att;
255        Message msg = Message.restoreMessageWithId(mContext, att.mMessageKey);
256        doProgressCallback(msg.mId, att.mId, 0);
257        DefaultHttpClient client = new DefaultHttpClient();
258        String us = makeUriString("GetAttachment", "&AttachmentName=" + att.mLocation);
259        HttpPost method = new HttpPost(URI.create(us));
260        method.setHeader("Authorization", mAuthString);
261
262        HttpResponse res = client.execute(method);
263        int status = res.getStatusLine().getStatusCode();
264        if (status == HttpURLConnection.HTTP_OK) {
265            HttpEntity e = res.getEntity();
266            int len = (int)e.getContentLength();
267            String type = e.getContentType().getValue();
268            InputStream is = res.getEntity().getContent();
269            File f = (req.destination != null)
270                    ? new File(req.destination)
271                    : createUniqueFileInternal(req.destination, att.mFileName);
272            if (f != null) {
273                // Ensure that the target directory exists
274                File destDir = f.getParentFile();
275                if (!destDir.exists()) {
276                    destDir.mkdirs();
277                }
278                FileOutputStream os = new FileOutputStream(f);
279                if (len > 0) {
280                    try {
281                        mPendingPartRequest = req;
282                        mPendingPartInputStream = is;
283                        byte[] bytes = new byte[CHUNK_SIZE];
284                        int length = len;
285                        while (len > 0) {
286                            int n = (len > CHUNK_SIZE ? CHUNK_SIZE : len);
287                            int read = is.read(bytes, 0, n);
288                            os.write(bytes, 0, read);
289                            len -= read;
290                            int pct = ((length - len) * 100 / length);
291                            doProgressCallback(msg.mId, att.mId, pct);
292                        }
293                    } finally {
294                        mPendingPartRequest = null;
295                        mPendingPartInputStream = null;
296                    }
297                }
298                os.flush();
299                os.close();
300
301                // EmailProvider will throw an exception if we try to update an unsaved attachment
302                if (att.isSaved()) {
303                    String contentUriString = (req.contentUriString != null)
304                            ? req.contentUriString
305                            : "file://" + f.getAbsolutePath();
306                    ContentValues cv = new ContentValues();
307                    cv.put(AttachmentColumns.CONTENT_URI, contentUriString);
308                    cv.put(AttachmentColumns.MIME_TYPE, type);
309                    att.update(mContext, cv);
310                    doStatusCallback(msg.mId, att.mId, EmailServiceStatus.SUCCESS);
311                }
312            }
313        } else {
314            doStatusCallback(msg.mId, att.mId, EmailServiceStatus.MESSAGE_NOT_FOUND);
315        }
316    }
317
318    @SuppressWarnings("deprecation")
319    private String makeUriString(String cmd, String extra) throws IOException {
320         // Cache the authentication string and the command string
321        String safeUserName = URLEncoder.encode(mUserName);
322        if (mAuthString == null) {
323            String cs = mUserName + ':' + mPassword;
324            mAuthString = "Basic " + Base64.encodeBytes(cs.getBytes());
325            mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId + "&DeviceType="
326                    + mDeviceType;
327        }
328        String us = (mSsl ? "https" : "http") + "://" + mHostAddress +
329            "/Microsoft-Server-ActiveSync";
330        if (cmd != null) {
331            us += "?Cmd=" + cmd + mCmdString;
332        }
333        if (extra != null) {
334            us += extra;
335        }
336        return us;
337    }
338
339    private void setHeaders(HttpRequestBase method) {
340        method.setHeader("Authorization", mAuthString);
341        method.setHeader("MS-ASProtocolVersion", mProtocolVersion);
342        method.setHeader("Connection", "keep-alive");
343        method.setHeader("User-Agent", mDeviceType + '/' + Eas.VERSION);
344    }
345
346    private HttpClient getHttpClient(int timeout) {
347        HttpParams params = new BasicHttpParams();
348        HttpConnectionParams.setConnectionTimeout(params, 10*SECS);
349        HttpConnectionParams.setSoTimeout(params, timeout);
350        return new DefaultHttpClient(params);
351    }
352
353    protected HttpResponse sendHttpClientPost(String cmd, byte[] bytes) throws IOException {
354        HttpClient client =
355            getHttpClient(cmd.equals(PING_COMMAND) ? PING_COMMAND_TIMEOUT : COMMAND_TIMEOUT);
356        String us = makeUriString(cmd, null);
357        HttpPost method = new HttpPost(URI.create(us));
358        if (cmd.startsWith("SendMail&")) {
359            method.setHeader("Content-Type", "message/rfc822");
360        } else {
361            method.setHeader("Content-Type", "application/vnd.ms-sync.wbxml");
362        }
363        setHeaders(method);
364        method.setEntity(new ByteArrayEntity(bytes));
365        return client.execute(method);
366    }
367
368    protected HttpResponse sendHttpClientOptions() throws IOException {
369        HttpClient client = getHttpClient(COMMAND_TIMEOUT);
370        String us = makeUriString("OPTIONS", null);
371        HttpOptions method = new HttpOptions(URI.create(us));
372        setHeaders(method);
373        return client.execute(method);
374    }
375
376    String getTargetCollectionClassFromCursor(Cursor c) {
377        int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
378        if (type == Mailbox.TYPE_CONTACTS) {
379            return "Contacts";
380        } else if (type == Mailbox.TYPE_CALENDAR) {
381            return "Calendar";
382        } else {
383            return "Email";
384        }
385    }
386
387    /**
388     * Performs FolderSync
389     *
390     * @throws IOException
391     * @throws EasParserException
392     */
393    public void runAccountMailbox() throws IOException, EasParserException {
394        // Initialize exit status to success
395        mExitStatus = EmailServiceStatus.SUCCESS;
396        try {
397            try {
398                SyncManager.callback()
399                    .syncMailboxListStatus(mAccount.mId, EmailServiceStatus.IN_PROGRESS, 0);
400            } catch (RemoteException e1) {
401                // Don't care if this fails
402            }
403
404            if (mAccount.mSyncKey == null) {
405                mAccount.mSyncKey = "0";
406                userLog("Account syncKey INIT to 0");
407                ContentValues cv = new ContentValues();
408                cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
409                mAccount.update(mContext, cv);
410            }
411
412            boolean firstSync = mAccount.mSyncKey.equals("0");
413            if (firstSync) {
414                userLog("Initial FolderSync");
415            }
416
417            // When we first start up, change all ping mailboxes to push.
418            ContentValues cv = new ContentValues();
419            cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH);
420            if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
421                    WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING,
422                    new String[] {Long.toString(mAccount.mId)}) > 0) {
423                SyncManager.kick("change ping boxes to push");
424            }
425
426            // Determine our protocol version, if we haven't already
427            if (mAccount.mProtocolVersion == null) {
428                userLog("Determine EAS protocol version");
429                HttpResponse resp = sendHttpClientOptions();
430                int code = resp.getStatusLine().getStatusCode();
431                userLog("OPTIONS response: " + code);
432                if (code == HttpURLConnection.HTTP_OK) {
433                    Header header = resp.getFirstHeader("ms-asprotocolversions");
434                    String versions = header.getValue();
435                    if (versions != null) {
436                        if (versions.contains("12.0")) {
437                            mProtocolVersion = "12.0";
438                        }
439                        mProtocolVersionDouble = Double.parseDouble(mProtocolVersion);
440                        mAccount.mProtocolVersion = mProtocolVersion;
441                        userLog(versions);
442                        userLog("Using version " + mProtocolVersion);
443                    } else {
444                        errorLog("No protocol versions in OPTIONS response");
445                        throw new IOException();
446                    }
447                } else {
448                    errorLog("OPTIONS command failed; throwing IOException");
449                    throw new IOException();
450                }
451            }
452
453             while (!mStop) {
454                 userLog("Sending Account syncKey: " + mAccount.mSyncKey);
455                 Serializer s = new Serializer();
456                 s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY)
457                 .text(mAccount.mSyncKey).end().end().done();
458                 HttpResponse resp = sendHttpClientPost("FolderSync", s.toByteArray());
459                 if (mStop) break;
460                 int code = resp.getStatusLine().getStatusCode();
461                 if (code == HttpURLConnection.HTTP_OK) {
462                     HttpEntity entity = resp.getEntity();
463                     int len = (int)entity.getContentLength();
464                     if (len > 0) {
465                         InputStream is = entity.getContent();
466                         // Returns true if we need to sync again
467                         if (new FolderSyncParser(is, this).parse()) {
468                             continue;
469                         }
470                     }
471                 } else if (code == HttpURLConnection.HTTP_UNAUTHORIZED ||
472                        code == HttpURLConnection.HTTP_FORBIDDEN) {
473                    mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE;
474                } else {
475                    userLog("FolderSync response error: " + code);
476                }
477
478                // Change all push/hold boxes to push
479                cv = new ContentValues();
480                cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH);
481                if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
482                        WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX,
483                        new String[] {Long.toString(mAccount.mId)}) > 0) {
484                    userLog("Set push/hold boxes to push...");
485                }
486
487                try {
488                    SyncManager.callback()
489                        .syncMailboxListStatus(mAccount.mId, mExitStatus, 0);
490                } catch (RemoteException e1) {
491                    // Don't care if this fails
492                }
493
494                // Wait for push notifications.
495                String threadName = Thread.currentThread().getName();
496                try {
497                    runPingLoop();
498                } catch (StaleFolderListException e) {
499                    // We break out if we get told about a stale folder list
500                    userLog("Ping interrupted; folder list requires sync...");
501                } finally {
502                    Thread.currentThread().setName(threadName);
503                }
504            }
505         } catch (IOException e) {
506            // We catch this here to send the folder sync status callback
507            // A folder sync failed callback will get sent from run()
508            try {
509                if (!mStop) {
510                    SyncManager.callback()
511                        .syncMailboxListStatus(mAccount.mId,
512                                EmailServiceStatus.CONNECTION_ERROR, 0);
513                }
514            } catch (RemoteException e1) {
515                // Don't care if this fails
516            }
517            throw new IOException();
518        }
519    }
520
521    void pushFallback() {
522        // We'll try reloading folders first; this has been observed to work in some cases
523        if (!mTriedReloadFolderList) {
524            errorLog("*** PING LOOP: Trying to reload folder list...");
525            SyncManager.reloadFolderList(mContext, mAccount.mId, true);
526            mTriedReloadFolderList = true;
527        // If we've tried that, set all mailboxes (except the account mailbox) to 5 minute sync
528        } else {
529            errorLog("*** PING LOOP: Turning off push due to ping loop...");
530            ContentValues cv = new ContentValues();
531            cv.put(Mailbox.SYNC_INTERVAL, 5);
532            mContentResolver.update(Mailbox.CONTENT_URI, cv,
533                    MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId
534                    + AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null);
535            // Now, change the account as well
536            cv.clear();
537            cv.put(Account.SYNC_INTERVAL, 5);
538            mContentResolver.update(ContentUris.withAppendedId(Account.CONTENT_URI, mAccount.mId),
539                    cv, null, null);
540            // TODO Discuss the best way to alert the user
541            // Alert the user about what we've done
542            NotificationManager nm = (NotificationManager)mContext
543                .getSystemService(Context.NOTIFICATION_SERVICE);
544            Notification note =
545                new Notification(R.drawable.stat_notify_email_generic,
546                        mContext.getString(R.string.notification_ping_loop_title),
547                        System.currentTimeMillis());
548            Intent i = new Intent(mContext, AccountFolderList.class);
549            PendingIntent pi = PendingIntent.getActivity(mContext, 0, i, 0);
550            note.setLatestEventInfo(mContext,
551                    mContext.getString(R.string.notification_ping_loop_title),
552                    mContext.getString(R.string.notification_ping_loop_text), pi);
553            nm.notify(Eas.EXCHANGE_ERROR_NOTIFICATION, note);
554        }
555    }
556
557    void runPingLoop() throws IOException, StaleFolderListException {
558        // Do push for all sync services here
559        ArrayList<Mailbox> pushBoxes = new ArrayList<Mailbox>();
560        long endTime = System.currentTimeMillis() + (30*MINS);
561        HashMap<Long, Integer> pingFailureMap = new HashMap<Long, Integer>();
562
563        while (System.currentTimeMillis() < endTime) {
564            // Count of pushable mailboxes
565            int pushCount = 0;
566            // Count of mailboxes that can be pushed right now
567            int canPushCount = 0;
568            Serializer s = new Serializer();
569            int code;
570            Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
571                    MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId +
572                    AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null, null);
573
574            pushBoxes.clear();
575
576            try {
577                // Loop through our pushed boxes seeing what is available to push
578                while (c.moveToNext()) {
579                    pushCount++;
580                    // Two requirements for push:
581                    // 1) SyncManager tells us the mailbox is syncable (not running, not stopped)
582                    // 2) The syncKey isn't "0" (i.e. it's synced at least once)
583                    long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN);
584                    if (SyncManager.canSync(mailboxId)) {
585                        String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN);
586                        if (syncKey == null || syncKey.equals("0")) {
587                            continue;
588                        }
589
590                        // Take a peek at this box's behavior last sync
591                        // We do this because some Exchange 2003 servers put themselves (and
592                        // therefore our client) into a "ping loop" in which the client is
593                        // continuously told of server changes, only to find that there aren't any.
594                        // This behavior is seemingly random, and we must code defensively by
595                        // backing off of push behavior when this is detected.
596                        // The server fix is at http://support.microsoft.com/kb/923282
597
598                        // Sync status is encoded as S<type>:<exitstatus>:<changes>
599                        String status = c.getString(Mailbox.CONTENT_SYNC_STATUS_COLUMN);
600                        int type = SyncManager.getStatusType(status);
601                        if (type == SyncManager.SYNC_PING) {
602                            int changeCount = SyncManager.getStatusChangeCount(status);
603                            if (changeCount == 0) {
604                                // This means that a ping failed; we'll keep track of this
605                                Integer failures = pingFailureMap.get(mailboxId);
606                                if (failures == null) {
607                                    pingFailureMap.put(mailboxId, 1);
608                                } else if (failures > 4) {
609                                    // Change all push/ping boxes (except account) to 5 minute sync
610                                    pushFallback();
611                                    return;
612                                } else {
613                                    pingFailureMap.put(mailboxId, failures + 1);
614                                }
615                            } else {
616                                pingFailureMap.put(mailboxId, 0);
617                            }
618                        }
619
620                        if (canPushCount++ == 0) {
621                            // Initialize the Ping command
622                            s.start(Tags.PING_PING).data(Tags.PING_HEARTBEAT_INTERVAL, "900")
623                                .start(Tags.PING_FOLDERS);
624                        }
625                        // When we're ready for Calendar/Contacts, we will check folder type
626                        // TODO Save Calendar and Contacts!! Mark as not visible!
627                        String folderClass = getTargetCollectionClassFromCursor(c);
628                        s.start(Tags.PING_FOLDER)
629                            .data(Tags.PING_ID, c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN))
630                            .data(Tags.PING_CLASS, folderClass)
631                            .end();
632                        userLog("Ping ready for: " + folderClass + ", " +
633                                c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN) + " (" +
634                                c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN) + ')');
635                        pushBoxes.add(new Mailbox().restore(c));
636                    } else {
637                        userLog(c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN) +
638                                " not ready for ping");
639                    }
640                }
641            } finally {
642                c.close();
643            }
644
645            if (canPushCount > 0 && (canPushCount == pushCount)) {
646                // If we have some number that are ready for push, send Ping to the server
647                s.end().end().done();
648
649                HttpResponse res = sendHttpClientPost(PING_COMMAND, s.toByteArray());
650                Thread.currentThread().setName(mAccount.mDisplayName + ": Ping");
651                //userLog("Sending ping, timeout: " + uc.getReadTimeout() / 1000 + "s");
652
653                // Don't send request if we've been asked to stop
654                if (mStop) return;
655                long time = System.currentTimeMillis();
656                code = res.getStatusLine().getStatusCode();
657
658                // Return immediately if we've been asked to stop
659                if (mStop) {
660                    userLog("Stopping pingLoop");
661                    return;
662                }
663
664                // Get elapsed time
665                time = System.currentTimeMillis() - time;
666                userLog("Ping response: " + code + " in " + time + "ms");
667
668                if (code == HttpURLConnection.HTTP_OK) {
669                    HttpEntity e = res.getEntity();
670                    int len = (int)e.getContentLength();
671                    InputStream is = res.getEntity().getContent();
672                    if (len > 0) {
673                        parsePingResult(is, mContentResolver);
674                    } else {
675                        throw new IOException();
676                    }
677                } else if (isAuthError(code)) {
678                    mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE;
679                    userLog("Authorization error during Ping: " + code);
680                    throw new IOException();
681                }
682            } else if (pushCount > 0) {
683                // If we want to Ping, but can't just yet, wait 10 seconds and try again
684                userLog("pingLoop waiting for " + (pushCount - canPushCount) + " box(es)");
685                sleep(10*SECS);
686            } else {
687                // We've got nothing to do, so let's hang out for a while
688                sleep(20*MINS);
689            }
690        }
691    }
692
693    void sleep(long ms) {
694        try {
695            Thread.sleep(ms);
696        } catch (InterruptedException e) {
697            // Doesn't matter whether we stop early; it's the thought that counts
698        }
699    }
700
701    private int parsePingResult(InputStream is, ContentResolver cr)
702        throws IOException, StaleFolderListException {
703        PingParser pp = new PingParser(is, this);
704        if (pp.parse()) {
705            // True indicates some mailboxes need syncing...
706            // syncList has the serverId's of the mailboxes...
707            mBindArguments[0] = Long.toString(mAccount.mId);
708            ArrayList<String> syncList = pp.getSyncList();
709            for (String serverId: syncList) {
710                mBindArguments[1] = serverId;
711                Cursor c = cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
712                        WHERE_ACCOUNT_KEY_AND_SERVER_ID, mBindArguments, null);
713                try {
714                    if (c.moveToFirst()) {
715                        SyncManager.startManualSync(c.getLong(Mailbox.CONTENT_ID_COLUMN),
716                                SyncManager.SYNC_PING, null);
717                    }
718                } finally {
719                    c.close();
720                }
721            }
722        }
723        return pp.getSyncList().size();
724    }
725
726    ByteArrayInputStream readResponse(HttpURLConnection uc) throws IOException {
727        String encoding = uc.getHeaderField("Transfer-Encoding");
728        if (encoding == null) {
729            int len = uc.getHeaderFieldInt("Content-Length", 0);
730            if (len > 0) {
731                InputStream in = uc.getInputStream();
732                byte[] bytes = new byte[len];
733                int remain = len;
734                int offs = 0;
735                while (remain > 0) {
736                    int read = in.read(bytes, offs, remain);
737                    remain -= read;
738                    offs += read;
739                }
740                return new ByteArrayInputStream(bytes);
741            }
742        } else if (encoding.equalsIgnoreCase("chunked")) {
743            // TODO We don't handle this yet
744            return null;
745        }
746        return null;
747    }
748
749    String readResponseString(HttpURLConnection uc) throws IOException {
750        String encoding = uc.getHeaderField("Transfer-Encoding");
751        if (encoding == null) {
752            int len = uc.getHeaderFieldInt("Content-Length", 0);
753            if (len > 0) {
754                InputStream in = uc.getInputStream();
755                byte[] bytes = new byte[len];
756                int remain = len;
757                int offs = 0;
758                while (remain > 0) {
759                    int read = in.read(bytes, offs, remain);
760                    remain -= read;
761                    offs += read;
762                }
763                return new String(bytes);
764            }
765        } else if (encoding.equalsIgnoreCase("chunked")) {
766            // TODO We don't handle this yet
767            return null;
768        }
769        return null;
770    }
771
772    private String getFilterType() {
773        String filter = Eas.FILTER_1_WEEK;
774        switch (mAccount.mSyncLookback) {
775            case com.android.email.Account.SYNC_WINDOW_1_DAY: {
776                filter = Eas.FILTER_1_DAY;
777                break;
778            }
779            case com.android.email.Account.SYNC_WINDOW_3_DAYS: {
780                filter = Eas.FILTER_3_DAYS;
781                break;
782            }
783            case com.android.email.Account.SYNC_WINDOW_1_WEEK: {
784                filter = Eas.FILTER_1_WEEK;
785                break;
786            }
787            case com.android.email.Account.SYNC_WINDOW_2_WEEKS: {
788                filter = Eas.FILTER_2_WEEKS;
789                break;
790            }
791            case com.android.email.Account.SYNC_WINDOW_1_MONTH: {
792                filter = Eas.FILTER_1_MONTH;
793                break;
794            }
795            case com.android.email.Account.SYNC_WINDOW_ALL: {
796                filter = Eas.FILTER_ALL;
797                break;
798            }
799        }
800        return filter;
801    }
802
803    /**
804     * Common code to sync E+PIM data
805     *
806     * @param target, an EasMailbox, EasContacts, or EasCalendar object
807     */
808    public void sync(AbstractSyncAdapter target) throws IOException {
809        mTarget = target;
810        Mailbox mailbox = target.mMailbox;
811
812        boolean moreAvailable = true;
813        while (!mStop && moreAvailable) {
814            runAwake();
815            waitForConnectivity();
816
817            while (true) {
818                PartRequest req = null;
819                synchronized (mPartRequests) {
820                    if (mPartRequests.isEmpty()) {
821                        break;
822                    } else {
823                        req = mPartRequests.get(0);
824                    }
825                }
826                getAttachment(req);
827                synchronized(mPartRequests) {
828                    mPartRequests.remove(req);
829                }
830            }
831
832            Serializer s = new Serializer();
833            if (mailbox.mSyncKey == null) {
834                userLog("Mailbox syncKey RESET");
835                mailbox.mSyncKey = "0";
836            }
837            String className = target.getCollectionName();
838            userLog("Sending " + className + " syncKey: " + mailbox.mSyncKey);
839            s.start(Tags.SYNC_SYNC)
840                .start(Tags.SYNC_COLLECTIONS)
841                .start(Tags.SYNC_COLLECTION)
842                .data(Tags.SYNC_CLASS, className)
843                .data(Tags.SYNC_SYNC_KEY, mailbox.mSyncKey)
844                .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId)
845                .tag(Tags.SYNC_DELETES_AS_MOVES);
846
847            // EAS doesn't like GetChanges if the syncKey is "0"; not documented
848            if (!mailbox.mSyncKey.equals("0")) {
849                s.tag(Tags.SYNC_GET_CHANGES);
850            }
851            s.data(Tags.SYNC_WINDOW_SIZE,
852                    className.equals("Email") ? EMAIL_WINDOW_SIZE : PIM_WINDOW_SIZE);
853            boolean options = false;
854            if (!className.equals("Contacts")) {
855                // Set the lookback appropriately (EAS calls this a "filter")
856                s.start(Tags.SYNC_OPTIONS).data(Tags.SYNC_FILTER_TYPE, getFilterType());
857                // No truncation in this version
858                //if (mProtocolVersionDouble < 12.0) {
859                //    s.data(Tags.SYNC_TRUNCATION, "7");
860                //}
861                options = true;
862            }
863            if (mProtocolVersionDouble >= 12.0) {
864                if (!options) {
865                    options = true;
866                    s.start(Tags.SYNC_OPTIONS);
867                }
868                s.start(Tags.BASE_BODY_PREFERENCE)
869                    // HTML for email; plain text for everything else
870                .data(Tags.BASE_TYPE, (className.equals("Email") ? Eas.BODY_PREFERENCE_HTML
871                            : Eas.BODY_PREFERENCE_TEXT))
872                // No truncation in this version
873                //.data(Tags.BASE_TRUNCATION_SIZE, Eas.DEFAULT_BODY_TRUNCATION_SIZE)
874                    .end();
875            }
876            if (options) {
877                s.end();
878            }
879
880            // Send our changes up to the server
881            target.sendLocalChanges(s, this);
882
883            s.end().end().end().done();
884            HttpResponse resp = sendHttpClientPost("Sync", s.toByteArray());
885            int code = resp.getStatusLine().getStatusCode();
886            if (code == HttpURLConnection.HTTP_OK) {
887                 InputStream is = resp.getEntity().getContent();
888                if (is != null) {
889                    moreAvailable = target.parse(is, this);
890                    target.cleanup(this);
891                }
892            } else {
893                userLog("Sync response error: " + code);
894                if (isAuthError(code)) {
895                    mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE;
896                }
897                return;
898            }
899        }
900    }
901
902    /* (non-Javadoc)
903     * @see java.lang.Runnable#run()
904     */
905    public void run() {
906        mThread = Thread.currentThread();
907        TAG = mThread.getName();
908
909        HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv);
910        mHostAddress = ha.mAddress;
911        mUserName = ha.mLogin;
912        mPassword = ha.mPassword;
913
914        try {
915            SyncManager.callback().syncMailboxStatus(mMailboxId, EmailServiceStatus.IN_PROGRESS, 0);
916        } catch (RemoteException e1) {
917            // Don't care if this fails
918        }
919
920        // Make sure account and mailbox are always the latest from the database
921        mAccount = Account.restoreAccountWithId(mContext, mAccount.mId);
922        mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId);
923        // Whether or not we're the account mailbox
924        boolean accountMailbox = false;
925        try {
926            mDeviceId = SyncManager.getDeviceId();
927            if (mMailbox == null || mAccount == null) {
928                return;
929            } else if (mMailbox.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
930                accountMailbox = true;
931                runAccountMailbox();
932            } else {
933                AbstractSyncAdapter target;
934                mAccount = Account.restoreAccountWithId(mContext, mAccount.mId);
935                mProtocolVersion = mAccount.mProtocolVersion;
936                mProtocolVersionDouble = Double.parseDouble(mProtocolVersion);
937                if (mMailbox.mType == Mailbox.TYPE_CONTACTS)
938                    target = new ContactsSyncAdapter(mMailbox, this);
939                else {
940                    target = new EmailSyncAdapter(mMailbox, this);
941                }
942                // We loop here because someone might have put a request in while we were syncing
943                // and we've missed that opportunity...
944                do {
945                    if (mRequestTime != 0) {
946                        userLog("Looping for user request...");
947                        mRequestTime = 0;
948                    }
949                    sync(target);
950                } while (mRequestTime != 0);
951            }
952            mExitStatus = EXIT_DONE;
953        } catch (IOException e) {
954            userLog("Caught IOException");
955            mExitStatus = EXIT_IO_ERROR;
956        } catch (Exception e) {
957            e.printStackTrace();
958        } finally {
959            if (!mStop) {
960                userLog(mMailbox.mDisplayName + ": sync finished");
961                SyncManager.done(this);
962                // If this is the account mailbox, wake up SyncManager
963                // Because this box has a "push" interval, it will be restarted immediately
964                // which will cause the folder list to be reloaded...
965                try {
966                    int status;
967                    switch (mExitStatus) {
968                        case EXIT_IO_ERROR:
969                            status = EmailServiceStatus.CONNECTION_ERROR;
970                            break;
971                        case EXIT_DONE:
972                            status = EmailServiceStatus.SUCCESS;
973                            break;
974                        case EXIT_LOGIN_FAILURE:
975                            status = EmailServiceStatus.LOGIN_FAILED;
976                            break;
977                        default:
978                            status = EmailServiceStatus.REMOTE_EXCEPTION;
979                            break;
980                    }
981                    SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0);
982
983                    // Save the sync time and status
984                    ContentValues cv = new ContentValues();
985                    cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
986                    String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount;
987                    cv.put(Mailbox.SYNC_STATUS, s);
988                    mContentResolver.update(ContentUris
989                            .withAppendedId(Mailbox.CONTENT_URI, mMailboxId), cv, null, null);
990                } catch (RemoteException e1) {
991                    // Don't care if this fails
992                }
993            } else {
994                userLog(mMailbox.mDisplayName + ": stopped thread finished.");
995            }
996
997            // Make sure this gets restarted...
998            if (accountMailbox) {
999                SyncManager.kick("account mailbox stopped");
1000            }
1001       }
1002    }
1003}
1004