EasSyncService.java revision f708e075473f4c186c44b61bc5ad5c73c901b61e
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                         userLog("FolderSync, deviceId = " + mDeviceId);
468                         if (new FolderSyncParser(is, this).parse()) {
469                             continue;
470                         }
471                     }
472                 } else if (code == HttpURLConnection.HTTP_UNAUTHORIZED ||
473                        code == HttpURLConnection.HTTP_FORBIDDEN) {
474                    mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE;
475                } else {
476                    userLog("FolderSync response error: " + code);
477                }
478
479                // Change all push/hold boxes to push
480                cv = new ContentValues();
481                cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH);
482                if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
483                        WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX,
484                        new String[] {Long.toString(mAccount.mId)}) > 0) {
485                    userLog("Set push/hold boxes to push...");
486                }
487
488                try {
489                    SyncManager.callback()
490                        .syncMailboxListStatus(mAccount.mId, mExitStatus, 0);
491                } catch (RemoteException e1) {
492                    // Don't care if this fails
493                }
494
495                // Wait for push notifications.
496                String threadName = Thread.currentThread().getName();
497                try {
498                    runPingLoop();
499                } catch (StaleFolderListException e) {
500                    // We break out if we get told about a stale folder list
501                    userLog("Ping interrupted; folder list requires sync...");
502                } finally {
503                    Thread.currentThread().setName(threadName);
504                }
505            }
506         } catch (IOException e) {
507            // We catch this here to send the folder sync status callback
508            // A folder sync failed callback will get sent from run()
509            try {
510                if (!mStop) {
511                    SyncManager.callback()
512                        .syncMailboxListStatus(mAccount.mId,
513                                EmailServiceStatus.CONNECTION_ERROR, 0);
514                }
515            } catch (RemoteException e1) {
516                // Don't care if this fails
517            }
518            throw new IOException();
519        }
520    }
521
522    void pushFallback() {
523        // We'll try reloading folders first; this has been observed to work in some cases
524        if (!mTriedReloadFolderList) {
525            errorLog("*** PING LOOP: Trying to reload folder list...");
526            SyncManager.reloadFolderList(mContext, mAccount.mId, true);
527            mTriedReloadFolderList = true;
528        // If we've tried that, set all mailboxes (except the account mailbox) to 5 minute sync
529        } else {
530            errorLog("*** PING LOOP: Turning off push due to ping loop...");
531            ContentValues cv = new ContentValues();
532            cv.put(Mailbox.SYNC_INTERVAL, 5);
533            mContentResolver.update(Mailbox.CONTENT_URI, cv,
534                    MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId
535                    + AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null);
536            // Now, change the account as well
537            cv.clear();
538            cv.put(Account.SYNC_INTERVAL, 5);
539            mContentResolver.update(ContentUris.withAppendedId(Account.CONTENT_URI, mAccount.mId),
540                    cv, null, null);
541            // TODO Discuss the best way to alert the user
542            // Alert the user about what we've done
543            NotificationManager nm = (NotificationManager)mContext
544                .getSystemService(Context.NOTIFICATION_SERVICE);
545            Notification note =
546                new Notification(R.drawable.stat_notify_email_generic,
547                        mContext.getString(R.string.notification_ping_loop_title),
548                        System.currentTimeMillis());
549            Intent i = new Intent(mContext, AccountFolderList.class);
550            PendingIntent pi = PendingIntent.getActivity(mContext, 0, i, 0);
551            note.setLatestEventInfo(mContext,
552                    mContext.getString(R.string.notification_ping_loop_title),
553                    mContext.getString(R.string.notification_ping_loop_text), pi);
554            nm.notify(Eas.EXCHANGE_ERROR_NOTIFICATION, note);
555        }
556    }
557
558    void runPingLoop() throws IOException, StaleFolderListException {
559        // Do push for all sync services here
560        ArrayList<Mailbox> pushBoxes = new ArrayList<Mailbox>();
561        long endTime = System.currentTimeMillis() + (30*MINS);
562        HashMap<Long, Integer> pingFailureMap = new HashMap<Long, Integer>();
563
564        while (System.currentTimeMillis() < endTime) {
565            // Count of pushable mailboxes
566            int pushCount = 0;
567            // Count of mailboxes that can be pushed right now
568            int canPushCount = 0;
569            Serializer s = new Serializer();
570            int code;
571            Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
572                    MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId +
573                    AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null, null);
574
575            pushBoxes.clear();
576
577            try {
578                // Loop through our pushed boxes seeing what is available to push
579                while (c.moveToNext()) {
580                    pushCount++;
581                    // Two requirements for push:
582                    // 1) SyncManager tells us the mailbox is syncable (not running, not stopped)
583                    // 2) The syncKey isn't "0" (i.e. it's synced at least once)
584                    long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN);
585                    if (SyncManager.canSync(mailboxId)) {
586                        String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN);
587                        if (syncKey == null || syncKey.equals("0")) {
588                            continue;
589                        }
590
591                        // Take a peek at this box's behavior last sync
592                        // We do this because some Exchange 2003 servers put themselves (and
593                        // therefore our client) into a "ping loop" in which the client is
594                        // continuously told of server changes, only to find that there aren't any.
595                        // This behavior is seemingly random, and we must code defensively by
596                        // backing off of push behavior when this is detected.
597                        // The server fix is at http://support.microsoft.com/kb/923282
598
599                        // Sync status is encoded as S<type>:<exitstatus>:<changes>
600                        String status = c.getString(Mailbox.CONTENT_SYNC_STATUS_COLUMN);
601                        int type = SyncManager.getStatusType(status);
602                        if (type == SyncManager.SYNC_PING) {
603                            int changeCount = SyncManager.getStatusChangeCount(status);
604                            if (changeCount == 0) {
605                                // This means that a ping failed; we'll keep track of this
606                                Integer failures = pingFailureMap.get(mailboxId);
607                                if (failures == null) {
608                                    pingFailureMap.put(mailboxId, 1);
609                                } else if (failures > 4) {
610                                    // Change all push/ping boxes (except account) to 5 minute sync
611                                    pushFallback();
612                                    return;
613                                } else {
614                                    pingFailureMap.put(mailboxId, failures + 1);
615                                }
616                            } else {
617                                pingFailureMap.put(mailboxId, 0);
618                            }
619                        }
620
621                        if (canPushCount++ == 0) {
622                            // Initialize the Ping command
623                            s.start(Tags.PING_PING).data(Tags.PING_HEARTBEAT_INTERVAL, "900")
624                                .start(Tags.PING_FOLDERS);
625                        }
626                        // When we're ready for Calendar/Contacts, we will check folder type
627                        // TODO Save Calendar and Contacts!! Mark as not visible!
628                        String folderClass = getTargetCollectionClassFromCursor(c);
629                        s.start(Tags.PING_FOLDER)
630                            .data(Tags.PING_ID, c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN))
631                            .data(Tags.PING_CLASS, folderClass)
632                            .end();
633                        userLog("Ping ready for: " + folderClass + ", " +
634                                c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN) + " (" +
635                                c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN) + ')');
636                        pushBoxes.add(new Mailbox().restore(c));
637                    } else {
638                        userLog(c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN) +
639                                " not ready for ping");
640                    }
641                }
642            } finally {
643                c.close();
644            }
645
646            if (canPushCount > 0 && (canPushCount == pushCount)) {
647                // If we have some number that are ready for push, send Ping to the server
648                s.end().end().done();
649
650                Thread.currentThread().setName(mAccount.mDisplayName + ": Ping");
651                userLog("Sending ping, timeout: " + PING_COMMAND_TIMEOUT / MINS + "m");
652
653                SyncManager.runAsleep(mMailboxId, PING_COMMAND_TIMEOUT);
654                HttpResponse res = sendHttpClientPost(PING_COMMAND, s.toByteArray());
655                SyncManager.runAwake(mMailboxId);
656
657                // Don't send request if we've been asked to stop
658                if (mStop) return;
659                long time = System.currentTimeMillis();
660                code = res.getStatusLine().getStatusCode();
661
662                // Return immediately if we've been asked to stop
663                if (mStop) {
664                    userLog("Stopping pingLoop");
665                    return;
666                }
667
668                // Get elapsed time
669                time = System.currentTimeMillis() - time;
670                userLog("Ping response: " + code + " in " + time + "ms");
671
672                if (code == HttpURLConnection.HTTP_OK) {
673                    HttpEntity e = res.getEntity();
674                    int len = (int)e.getContentLength();
675                    InputStream is = res.getEntity().getContent();
676                    if (len > 0) {
677                        parsePingResult(is, mContentResolver);
678                    } else {
679                        throw new IOException();
680                    }
681                } else if (isAuthError(code)) {
682                    mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE;
683                    userLog("Authorization error during Ping: " + code);
684                    throw new IOException();
685                }
686            } else if (pushCount > 0) {
687                // If we want to Ping, but can't just yet, wait 10 seconds and try again
688                userLog("pingLoop waiting for " + (pushCount - canPushCount) + " box(es)");
689                sleep(10*SECS);
690            } else {
691                // We've got nothing to do, so let's hang out for a while
692                sleep(20*MINS);
693            }
694        }
695    }
696
697    void sleep(long ms) {
698        try {
699            Thread.sleep(ms);
700        } catch (InterruptedException e) {
701            // Doesn't matter whether we stop early; it's the thought that counts
702        }
703    }
704
705    private int parsePingResult(InputStream is, ContentResolver cr)
706        throws IOException, StaleFolderListException {
707        PingParser pp = new PingParser(is, this);
708        if (pp.parse()) {
709            // True indicates some mailboxes need syncing...
710            // syncList has the serverId's of the mailboxes...
711            mBindArguments[0] = Long.toString(mAccount.mId);
712            ArrayList<String> syncList = pp.getSyncList();
713            for (String serverId: syncList) {
714                mBindArguments[1] = serverId;
715                Cursor c = cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
716                        WHERE_ACCOUNT_KEY_AND_SERVER_ID, mBindArguments, null);
717                try {
718                    if (c.moveToFirst()) {
719                        SyncManager.startManualSync(c.getLong(Mailbox.CONTENT_ID_COLUMN),
720                                SyncManager.SYNC_PING, null);
721                    }
722                } finally {
723                    c.close();
724                }
725            }
726        }
727        return pp.getSyncList().size();
728    }
729
730    ByteArrayInputStream readResponse(HttpURLConnection uc) throws IOException {
731        String encoding = uc.getHeaderField("Transfer-Encoding");
732        if (encoding == null) {
733            int len = uc.getHeaderFieldInt("Content-Length", 0);
734            if (len > 0) {
735                InputStream in = uc.getInputStream();
736                byte[] bytes = new byte[len];
737                int remain = len;
738                int offs = 0;
739                while (remain > 0) {
740                    int read = in.read(bytes, offs, remain);
741                    remain -= read;
742                    offs += read;
743                }
744                return new ByteArrayInputStream(bytes);
745            }
746        } else if (encoding.equalsIgnoreCase("chunked")) {
747            // TODO We don't handle this yet
748            return null;
749        }
750        return null;
751    }
752
753    String readResponseString(HttpURLConnection uc) throws IOException {
754        String encoding = uc.getHeaderField("Transfer-Encoding");
755        if (encoding == null) {
756            int len = uc.getHeaderFieldInt("Content-Length", 0);
757            if (len > 0) {
758                InputStream in = uc.getInputStream();
759                byte[] bytes = new byte[len];
760                int remain = len;
761                int offs = 0;
762                while (remain > 0) {
763                    int read = in.read(bytes, offs, remain);
764                    remain -= read;
765                    offs += read;
766                }
767                return new String(bytes);
768            }
769        } else if (encoding.equalsIgnoreCase("chunked")) {
770            // TODO We don't handle this yet
771            return null;
772        }
773        return null;
774    }
775
776    private String getFilterType() {
777        String filter = Eas.FILTER_1_WEEK;
778        switch (mAccount.mSyncLookback) {
779            case com.android.email.Account.SYNC_WINDOW_1_DAY: {
780                filter = Eas.FILTER_1_DAY;
781                break;
782            }
783            case com.android.email.Account.SYNC_WINDOW_3_DAYS: {
784                filter = Eas.FILTER_3_DAYS;
785                break;
786            }
787            case com.android.email.Account.SYNC_WINDOW_1_WEEK: {
788                filter = Eas.FILTER_1_WEEK;
789                break;
790            }
791            case com.android.email.Account.SYNC_WINDOW_2_WEEKS: {
792                filter = Eas.FILTER_2_WEEKS;
793                break;
794            }
795            case com.android.email.Account.SYNC_WINDOW_1_MONTH: {
796                filter = Eas.FILTER_1_MONTH;
797                break;
798            }
799            case com.android.email.Account.SYNC_WINDOW_ALL: {
800                filter = Eas.FILTER_ALL;
801                break;
802            }
803        }
804        return filter;
805    }
806
807    /**
808     * Common code to sync E+PIM data
809     *
810     * @param target, an EasMailbox, EasContacts, or EasCalendar object
811     */
812    public void sync(AbstractSyncAdapter target) throws IOException {
813        mTarget = target;
814        Mailbox mailbox = target.mMailbox;
815
816        boolean moreAvailable = true;
817        while (!mStop && moreAvailable) {
818            waitForConnectivity();
819
820            while (true) {
821                PartRequest req = null;
822                synchronized (mPartRequests) {
823                    if (mPartRequests.isEmpty()) {
824                        break;
825                    } else {
826                        req = mPartRequests.get(0);
827                    }
828                }
829                getAttachment(req);
830                synchronized(mPartRequests) {
831                    mPartRequests.remove(req);
832                }
833            }
834
835            Serializer s = new Serializer();
836            if (mailbox.mSyncKey == null) {
837                userLog("Mailbox syncKey RESET");
838                mailbox.mSyncKey = "0";
839            }
840            String className = target.getCollectionName();
841            userLog("Sending " + className + " syncKey: " + mailbox.mSyncKey);
842            s.start(Tags.SYNC_SYNC)
843                .start(Tags.SYNC_COLLECTIONS)
844                .start(Tags.SYNC_COLLECTION)
845                .data(Tags.SYNC_CLASS, className)
846                .data(Tags.SYNC_SYNC_KEY, mailbox.mSyncKey)
847                .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId)
848                .tag(Tags.SYNC_DELETES_AS_MOVES);
849
850            // EAS doesn't like GetChanges if the syncKey is "0"; not documented
851            if (!mailbox.mSyncKey.equals("0")) {
852                s.tag(Tags.SYNC_GET_CHANGES);
853            }
854            s.data(Tags.SYNC_WINDOW_SIZE,
855                    className.equals("Email") ? EMAIL_WINDOW_SIZE : PIM_WINDOW_SIZE);
856            boolean options = false;
857            if (!className.equals("Contacts")) {
858                // Set the lookback appropriately (EAS calls this a "filter")
859                s.start(Tags.SYNC_OPTIONS).data(Tags.SYNC_FILTER_TYPE, getFilterType());
860                // No truncation in this version
861                //if (mProtocolVersionDouble < 12.0) {
862                //    s.data(Tags.SYNC_TRUNCATION, "7");
863                //}
864                options = true;
865            }
866            if (mProtocolVersionDouble >= 12.0) {
867                if (!options) {
868                    options = true;
869                    s.start(Tags.SYNC_OPTIONS);
870                }
871                s.start(Tags.BASE_BODY_PREFERENCE)
872                    // HTML for email; plain text for everything else
873                .data(Tags.BASE_TYPE, (className.equals("Email") ? Eas.BODY_PREFERENCE_HTML
874                            : Eas.BODY_PREFERENCE_TEXT))
875                // No truncation in this version
876                //.data(Tags.BASE_TRUNCATION_SIZE, Eas.DEFAULT_BODY_TRUNCATION_SIZE)
877                    .end();
878            }
879            if (options) {
880                s.end();
881            }
882
883            // Send our changes up to the server
884            target.sendLocalChanges(s, this);
885
886            s.end().end().end().done();
887            HttpResponse resp = sendHttpClientPost("Sync", s.toByteArray());
888            int code = resp.getStatusLine().getStatusCode();
889            if (code == HttpURLConnection.HTTP_OK) {
890                 InputStream is = resp.getEntity().getContent();
891                if (is != null) {
892                    moreAvailable = target.parse(is, this);
893                    target.cleanup(this);
894                }
895            } else {
896                userLog("Sync response error: " + code);
897                if (isAuthError(code)) {
898                    mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE;
899                }
900                return;
901            }
902        }
903    }
904
905    /* (non-Javadoc)
906     * @see java.lang.Runnable#run()
907     */
908    public void run() {
909        mThread = Thread.currentThread();
910        TAG = mThread.getName();
911
912        HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv);
913        mHostAddress = ha.mAddress;
914        mUserName = ha.mLogin;
915        mPassword = ha.mPassword;
916
917        try {
918            SyncManager.callback().syncMailboxStatus(mMailboxId, EmailServiceStatus.IN_PROGRESS, 0);
919        } catch (RemoteException e1) {
920            // Don't care if this fails
921        }
922
923        // Make sure account and mailbox are always the latest from the database
924        mAccount = Account.restoreAccountWithId(mContext, mAccount.mId);
925        mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId);
926        // Whether or not we're the account mailbox
927        boolean accountMailbox = false;
928        try {
929            mDeviceId = SyncManager.getDeviceId();
930            if (mMailbox == null || mAccount == null) {
931                return;
932            } else if (mMailbox.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
933                accountMailbox = true;
934                runAccountMailbox();
935            } else {
936                AbstractSyncAdapter target;
937                mAccount = Account.restoreAccountWithId(mContext, mAccount.mId);
938                mProtocolVersion = mAccount.mProtocolVersion;
939                mProtocolVersionDouble = Double.parseDouble(mProtocolVersion);
940                if (mMailbox.mType == Mailbox.TYPE_CONTACTS)
941                    target = new ContactsSyncAdapter(mMailbox, this);
942                else {
943                    target = new EmailSyncAdapter(mMailbox, this);
944                }
945                // We loop here because someone might have put a request in while we were syncing
946                // and we've missed that opportunity...
947                do {
948                    if (mRequestTime != 0) {
949                        userLog("Looping for user request...");
950                        mRequestTime = 0;
951                    }
952                    sync(target);
953                } while (mRequestTime != 0);
954            }
955            mExitStatus = EXIT_DONE;
956        } catch (IOException e) {
957            userLog("Caught IOException");
958            mExitStatus = EXIT_IO_ERROR;
959        } catch (Exception e) {
960            e.printStackTrace();
961        } finally {
962            if (!mStop) {
963                userLog(mMailbox.mDisplayName + ": sync finished");
964                SyncManager.done(this);
965                // If this is the account mailbox, wake up SyncManager
966                // Because this box has a "push" interval, it will be restarted immediately
967                // which will cause the folder list to be reloaded...
968                try {
969                    int status;
970                    switch (mExitStatus) {
971                        case EXIT_IO_ERROR:
972                            status = EmailServiceStatus.CONNECTION_ERROR;
973                            break;
974                        case EXIT_DONE:
975                            status = EmailServiceStatus.SUCCESS;
976                            break;
977                        case EXIT_LOGIN_FAILURE:
978                            status = EmailServiceStatus.LOGIN_FAILED;
979                            break;
980                        default:
981                            status = EmailServiceStatus.REMOTE_EXCEPTION;
982                            break;
983                    }
984                    SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0);
985
986                    // Save the sync time and status
987                    ContentValues cv = new ContentValues();
988                    cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
989                    String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount;
990                    cv.put(Mailbox.SYNC_STATUS, s);
991                    mContentResolver.update(ContentUris
992                            .withAppendedId(Mailbox.CONTENT_URI, mMailboxId), cv, null, null);
993                } catch (RemoteException e1) {
994                    // Don't care if this fails
995                }
996            } else {
997                userLog(mMailbox.mDisplayName + ": stopped thread finished.");
998            }
999
1000            // Make sure this gets restarted...
1001            if (accountMailbox) {
1002                SyncManager.kick("account mailbox stopped");
1003            }
1004       }
1005    }
1006}
1007