EasSyncService.java revision ab701c862d6e380c8e09ac53ed493aec5d523d3d
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.mail.AuthenticationFailedException;
21import com.android.email.mail.MessagingException;
22import com.android.exchange.EmailContent.Account;
23import com.android.exchange.EmailContent.Attachment;
24import com.android.exchange.EmailContent.AttachmentColumns;
25import com.android.exchange.EmailContent.HostAuth;
26import com.android.exchange.EmailContent.Mailbox;
27import com.android.exchange.EmailContent.MailboxColumns;
28import com.android.exchange.EmailContent.Message;
29import com.android.exchange.adapter.EasContactsSyncAdapter;
30import com.android.exchange.adapter.EasEmailSyncAdapter;
31import com.android.exchange.adapter.EasFolderSyncParser;
32import com.android.exchange.adapter.EasPingParser;
33import com.android.exchange.adapter.EasSerializer;
34import com.android.exchange.adapter.EasSyncAdapter;
35import com.android.exchange.adapter.EasParser.EasParserException;
36import com.android.exchange.utility.Base64;
37
38import org.apache.http.HttpEntity;
39import org.apache.http.HttpResponse;
40import org.apache.http.client.methods.HttpPost;
41import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
42import org.apache.http.impl.client.DefaultHttpClient;
43
44import android.content.ContentResolver;
45import android.content.ContentValues;
46import android.content.Context;
47import android.database.Cursor;
48import android.os.RemoteException;
49import android.util.Log;
50
51import java.io.BufferedReader;
52import java.io.BufferedWriter;
53import java.io.ByteArrayInputStream;
54import java.io.File;
55import java.io.FileNotFoundException;
56import java.io.FileOutputStream;
57import java.io.FileReader;
58import java.io.FileWriter;
59import java.io.IOException;
60import java.io.InputStream;
61import java.io.OutputStreamWriter;
62import java.net.HttpURLConnection;
63import java.net.MalformedURLException;
64import java.net.ProtocolException;
65import java.net.URI;
66import java.net.URL;
67import java.net.URLEncoder;
68import java.util.ArrayList;
69
70import javax.net.ssl.HttpsURLConnection;
71
72public class EasSyncService extends InteractiveSyncService {
73
74    private static final String WINDOW_SIZE = "10";
75    private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID =
76        MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SERVER_ID + "=?";
77    private static final String WHERE_SYNC_FREQUENCY_PING =
78        Mailbox.SYNC_FREQUENCY + '=' + Account.CHECK_INTERVAL_PING;
79    private static final String AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX = " AND " +
80        MailboxColumns.SYNC_FREQUENCY + " IN (" + Account.CHECK_INTERVAL_PING +
81        ',' + Account.CHECK_INTERVAL_PUSH + ") AND " + MailboxColumns.SERVER_ID + "!=\"" +
82        Eas.ACCOUNT_MAILBOX + '\"';
83
84    static private final int CHUNK_SIZE = 16 * 1024;
85
86    // Reasonable default
87    String mProtocolVersion = "2.5";
88    public Double mProtocolVersionDouble;
89    static String mDeviceId = null;
90    static String mDeviceType = "Android";
91    EasSyncAdapter mTarget;
92    String mAuthString = null;
93    String mCmdString = null;
94    String mVersions;
95    public String mHostAddress;
96    public String mUserName;
97    public String mPassword;
98    String mDomain = null;
99    boolean mSentCommands;
100    boolean mIsIdle = false;
101    boolean mSsl = true;
102    public Context mContext;
103    public ContentResolver mContentResolver;
104    String[] mBindArguments = new String[2];
105    InputStream mPendingPartInputStream = null;
106    private boolean mStop = false;
107    private Object mWaitTarget = new Object();
108
109    public EasSyncService(Context _context, Mailbox _mailbox) {
110        super(_context, _mailbox);
111        mContext = _context;
112        mContentResolver = _context.getContentResolver();
113        HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv);
114        mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0;
115    }
116
117    private EasSyncService(String prefix) {
118        super(prefix);
119    }
120
121    public EasSyncService() {
122        this("EAS Validation");
123    }
124
125    @Override
126    public void ping() {
127        userLog("We've been pinged!");
128        synchronized (mWaitTarget) {
129            mWaitTarget.notify();
130        }
131    }
132
133    @Override
134    public void stop() {
135        mStop = true;
136    }
137
138    public int getSyncStatus() {
139        return 0;
140    }
141
142    /* (non-Javadoc)
143     * @see com.android.exchange.SyncService#validateAccount(java.lang.String, java.lang.String, java.lang.String, int, boolean, android.content.Context)
144     */
145    public void validateAccount(String hostAddress, String userName, String password, int port,
146            boolean ssl, Context context) throws MessagingException {
147        try {
148            if (Eas.USER_DEBUG) {
149                userLog("Testing EAS: " + hostAddress + ", " + userName + ", ssl = " + ssl);
150            }
151            EasSerializer s = new EasSerializer();
152            s.start("FolderSync").start("FolderSyncKey").text("0").end("FolderSyncKey")
153                .end("FolderSync").end();
154            EasSyncService svc = new EasSyncService("%TestAccount%");
155            svc.mHostAddress = hostAddress;
156            svc.mUserName = userName;
157            svc.mPassword = password;
158            svc.mSsl = ssl;
159            HttpURLConnection uc = svc.sendEASPostCommand("FolderSync", s.toString());
160            int code = uc.getResponseCode();
161            userLog("Validation response code: " + code);
162            if (code == HttpURLConnection.HTTP_OK) {
163                // No exception means successful validation
164                userLog("Validation successful");
165                return;
166            }
167            if (code == HttpURLConnection.HTTP_UNAUTHORIZED ||
168                    code == HttpURLConnection.HTTP_FORBIDDEN) {
169                userLog("Authentication failed");
170                throw new AuthenticationFailedException("Validation failed");
171            } else {
172                // TODO Need to catch other kinds of errors (e.g. policy) For now, report the code.
173                userLog("Validation failed, reporting I/O error: " + code);
174                throw new MessagingException(MessagingException.IOERROR);
175            }
176        } catch (IOException e) {
177            userLog("IOException caught, reporting I/O error: " + e.getMessage());
178            throw new MessagingException(MessagingException.IOERROR);
179        }
180
181    }
182
183
184    @Override
185    public void loadAttachment(Attachment att, IEmailServiceCallback cb) {
186        // TODO Auto-generated method stub
187    }
188
189    @Override
190    public void reloadFolderList() {
191        // TODO Auto-generated method stub
192    }
193
194    @Override
195    public void startSync() {
196        // TODO Auto-generated method stub
197    }
198
199    @Override
200    public void stopSync() {
201        // TODO Auto-generated method stub
202    }
203
204    protected HttpURLConnection sendEASPostCommand(String cmd, String data) throws IOException {
205        HttpURLConnection uc = setupEASCommand("POST", cmd);
206        if (uc != null) {
207            uc.setRequestProperty("Content-Length", Integer.toString(data.length() + 2));
208            OutputStreamWriter w = new OutputStreamWriter(uc.getOutputStream(), "UTF-8");
209            w.write(data);
210            w.write("\r\n");
211            w.flush();
212            w.close();
213        }
214        return uc;
215    }
216
217    private void doStatusCallback(IEmailServiceCallback callback, long messageId,
218            long attachmentId, int status) {
219        try {
220            callback.status(messageId, attachmentId, status, 0);
221        } catch (RemoteException e2) {
222            // No danger if the client is no longer around
223        }
224    }
225
226    private void doProgressCallback(IEmailServiceCallback callback, long messageId,
227            long attachmentId, int progress) {
228        try {
229            callback.status(messageId, attachmentId, EmailServiceStatus.IN_PROGRESS, progress);
230        } catch (RemoteException e2) {
231            // No danger if the client is no longer around
232        }
233    }
234
235    /**
236     * Loads an attachment, based on the PartRequest passed in.  The PartRequest is basically our
237     * wrapper for Attachment
238     * @param req the part (attachment) to be retrieved
239     * @param external whether the attachment should be loaded to external storage
240     * @throws IOException
241     */
242    protected void getAttachment(PartRequest req, boolean external) throws IOException {
243        // TODO Implement internal storage as required
244        IEmailServiceCallback callback = req.callback;
245        Attachment att = req.att;
246        Message msg = Message.restoreMessageWithId(mContext, att.mMessageKey);
247        doProgressCallback(callback, msg.mId, att.mId, 0);
248        DefaultHttpClient client = new DefaultHttpClient();
249        String us = makeUriString("GetAttachment", "&AttachmentName=" + att.mLocation);
250        HttpPost method = new HttpPost(URI.create(us));
251        method.setHeader("Authorization", mAuthString);
252
253        HttpResponse res = client.execute(method);
254        int status = res.getStatusLine().getStatusCode();
255        if (status == HttpURLConnection.HTTP_OK) {
256            HttpEntity e = res.getEntity();
257            int len = (int)e.getContentLength();
258            String type = e.getContentType().getValue();
259            if (Eas.TEST_DEBUG) {
260                Log.v(TAG, "Attachment code: " + status + ", Length: " + len + ", Type: " + type);
261            }
262            InputStream is = res.getEntity().getContent();
263            File f = Attachment.createUniqueFile(att.mFileName);
264            if (f != null) {
265                FileOutputStream os = new FileOutputStream(f);
266                if (len > 0) {
267                    try {
268                        mPendingPartRequest = req;
269                        mPendingPartInputStream = is;
270                        byte[] bytes = new byte[CHUNK_SIZE];
271                        int length = len;
272                        while (len > 0) {
273                            int n = (len > CHUNK_SIZE ? CHUNK_SIZE : len);
274                            int read = is.read(bytes, 0, n);
275                            os.write(bytes, 0, read);
276                            len -= read;
277                            int pct = ((length - len) * 100 / length);
278                            doProgressCallback(callback, msg.mId, att.mId, pct);
279                        }
280                    } finally {
281                        mPendingPartRequest = null;
282                        mPendingPartInputStream = null;
283                    }
284                }
285                os.flush();
286                os.close();
287
288                // EmailProvider will throw an exception if we try to update an unsaved attachment
289                if (att.isSaved()) {
290                    ContentValues cv = new ContentValues();
291                    cv.put(AttachmentColumns.CONTENT_URI, f.getAbsolutePath());
292                    cv.put(AttachmentColumns.MIME_TYPE, type);
293                    att.update(mContext, cv);
294                    doStatusCallback(callback, msg.mId, att.mId, EmailServiceStatus.SUCCESS);
295                }
296            }
297        } else {
298            doStatusCallback(callback, msg.mId, att.mId, EmailServiceStatus.MESSAGE_NOT_FOUND);
299        }
300    }
301
302    private HttpURLConnection setupEASCommand(String method, String cmd) throws IOException {
303        return setupEASCommand(method, cmd, null);
304    }
305
306    private String makeUriString(String cmd, String extra) {
307         // Cache the authentication string and the command string
308        if (mDeviceId == null)
309            mDeviceId = "droidfu";
310        String safeUserName = URLEncoder.encode(mUserName);
311        if (mAuthString == null) {
312            String cs = mUserName + ':' + mPassword;
313            mAuthString = "Basic " + Base64.encodeBytes(cs.getBytes());
314            mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId + "&DeviceType="
315                    + mDeviceType;
316        }
317
318        String us = (mSsl ? "https" : "http") + "://" + mHostAddress +
319            "/Microsoft-Server-ActiveSync";
320        if (cmd != null) {
321            us += "?Cmd=" + cmd + mCmdString;
322        }
323        if (extra != null) {
324            us += extra;
325        }
326        return us;
327    }
328
329    private HttpURLConnection setupEASCommand(String method, String cmd, String extra)
330            throws IOException {
331        try {
332            String us = makeUriString(cmd, extra);
333            URL u = new URL(us);
334            HttpURLConnection uc = (HttpURLConnection)u.openConnection();
335            HttpURLConnection.setFollowRedirects(true);
336
337            if (mSsl) {
338                ((HttpsURLConnection)uc).setHostnameVerifier(new AllowAllHostnameVerifier());
339            }
340
341            uc.setConnectTimeout(10 * SECS);
342            uc.setReadTimeout(20 * MINS);
343            if (method.equals("POST")) {
344                uc.setDoOutput(true);
345            }
346            uc.setRequestMethod(method);
347            uc.setRequestProperty("Authorization", mAuthString);
348
349            if (extra == null) {
350                if (cmd != null && cmd.startsWith("SendMail&")) {
351                    uc.setRequestProperty("Content-Type", "message/rfc822");
352                } else {
353                    uc.setRequestProperty("Content-Type", "application/vnd.ms-sync.wbxml");
354                }
355                uc.setRequestProperty("MS-ASProtocolVersion", mProtocolVersion);
356                uc.setRequestProperty("Connection", "keep-alive");
357                uc.setRequestProperty("User-Agent", mDeviceType + '/' + Eas.VERSION);
358            } else {
359                uc.setRequestProperty("Content-Length", "0");
360            }
361
362            return uc;
363        } catch (MalformedURLException e) {
364            // TODO See if there is a better exception to throw here and below
365            throw new IOException();
366        } catch (ProtocolException e) {
367            throw new IOException();
368        }
369    }
370
371    String getTargetCollectionClassFromCursor(Cursor c) {
372        int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
373        if (type == Mailbox.TYPE_CONTACTS) {
374            return "Contacts";
375        } else if (type == Mailbox.TYPE_CALENDAR) {
376            return "Calendar";
377        } else {
378            return "Email";
379        }
380    }
381
382    /**
383     * Performs FolderSync
384     *
385     * @throws IOException
386     * @throws EasParserException
387     */
388    public void runMain() throws IOException, EasParserException {
389        try {
390            if (mAccount.mSyncKey == null) {
391                mAccount.mSyncKey = "0";
392                userLog("Account syncKey RESET");
393                mAccount.saveOrUpdate(mContext);
394            }
395
396            // When we first start up, change all ping mailboxes to push.
397            ContentValues cv = new ContentValues();
398            cv.put(Mailbox.SYNC_FREQUENCY, Account.CHECK_INTERVAL_PUSH);
399            if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
400                    WHERE_SYNC_FREQUENCY_PING, null) > 0) {
401                SyncManager.kick();
402            }
403
404            userLog("Account syncKey: " + mAccount.mSyncKey);
405            HttpURLConnection uc = setupEASCommand("OPTIONS", null);
406            if (uc != null) {
407                int code = uc.getResponseCode();
408                userLog("OPTIONS response: " + code);
409                if (code == HttpURLConnection.HTTP_OK) {
410                    mVersions = uc.getHeaderField("ms-asprotocolversions");
411                    if (mVersions != null) {
412                        if (mVersions.contains("12.0")) {
413                            mProtocolVersion = "12.0";
414                        }
415                        mProtocolVersionDouble = Double.parseDouble(mProtocolVersion);
416                        mAccount.mProtocolVersion = mProtocolVersion;
417                        userLog(mVersions);
418                        userLog("Using version " + mProtocolVersion);
419                    } else {
420                        throw new IOException();
421                    }
422
423                    while (!mStop) {
424                        EasSerializer s = new EasSerializer();
425                        s.start("FolderSync").start("FolderSyncKey").text(mAccount.mSyncKey).end(
426                                "FolderSyncKey").end("FolderSync").end();
427                        uc = sendEASPostCommand("FolderSync", s.toString());
428                        code = uc.getResponseCode();
429                        if (code == HttpURLConnection.HTTP_OK) {
430                            String encoding = uc.getHeaderField("Transfer-Encoding");
431                            if (encoding == null) {
432                                int len = uc.getHeaderFieldInt("Content-Length", 0);
433                                if (len > 0) {
434                                    InputStream is = uc.getInputStream();
435                                    // Returns true if we need to sync again
436                                    if (new EasFolderSyncParser(is, this).parse()) {
437                                        continue;
438                                    }
439                                }
440                            } else if (encoding.equalsIgnoreCase("chunked")) {
441                                // TODO We don't handle this yet
442                            }
443                        } else {
444                            userLog("FolderSync response error: " + code);
445                        }
446
447                        // Wait for push notifications.
448                        String threadName = Thread.currentThread().getName();
449                        try {
450                            runPingLoop();
451                        } catch (StaleFolderListException e) {
452                            // We break out if we get told about a stale folder list
453                            userLog("Ping interrupted; folder list requires sync...");
454                        } finally {
455                            Thread.currentThread().setName(threadName);
456                        }
457                    }
458                 }
459            }
460        } catch (MalformedURLException e) {
461            throw new IOException();
462        }
463    }
464
465    void runPingLoop() throws IOException, StaleFolderListException {
466        // Do push for all sync services here
467        long endTime = System.currentTimeMillis() + (30*MINS);
468
469        while (System.currentTimeMillis() < endTime) {
470            // Count of pushable mailboxes
471            int pushCount = 0;
472            // Count of mailboxes that can be pushed right now
473            int canPushCount = 0;
474            EasSerializer s = new EasSerializer();
475            HttpURLConnection uc;
476            int code;
477            Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
478                    MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId +
479                    AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null, null);
480
481            try {
482                // Loop through our pushed boxes seeing what is available to push
483                while (c.moveToNext()) {
484                    pushCount++;
485                    // Two requirements for push:
486                    // 1) SyncManager tells us the mailbox is syncable (not running, not stopped)
487                    // 2) The syncKey isn't "0" (i.e. it's synced at least once)
488                    if (SyncManager.canSync(c.getLong(Mailbox.CONTENT_ID_COLUMN))) {
489                        String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN);
490                        if (syncKey == null || syncKey.equals("0")) {
491                            continue;
492                        }
493                        if (canPushCount++ == 0) {
494                            // Initialize the Ping command
495                            s.start("Ping").data("HeartbeatInterval", "900").start("PingFolders");
496                        }
497                        // When we're ready for Calendar/Contacts, we will check folder type
498                        // TODO Save Calendar and Contacts!! Mark as not visible!
499                        String folderClass = getTargetCollectionClassFromCursor(c);
500                        s.start("PingFolder")
501                            .data("PingId", c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN))
502                            .data("PingClass", folderClass)
503                            .end("PingFolder");
504                        userLog("Ping ready for: " + folderClass + ", " +
505                                c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN) + " (" +
506                                c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN) + ')');
507                    } else {
508                        userLog(c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN) +
509                                " not ready for ping");
510                    }
511                }
512            } finally {
513                c.close();
514            }
515
516            if (canPushCount > 0 && (canPushCount == pushCount)) {
517                // If we have some number that are ready for push, send Ping to the server
518                s.end("PingFolders").end("Ping").end();
519                uc = sendEASPostCommand("Ping", s.toString());
520                Thread.currentThread().setName(mAccount.mDisplayName + ": Ping");
521                userLog("Sending ping, timeout: " + uc.getReadTimeout() / 1000 + "s");
522                code = uc.getResponseCode();
523                userLog("Ping response: " + code);
524                if (code == HttpURLConnection.HTTP_OK) {
525                    String encoding = uc.getHeaderField("Transfer-Encoding");
526                    if (encoding == null) {
527                        int len = uc.getHeaderFieldInt("Content-Length", 0);
528                        if (len > 0) {
529                            parsePingResult(uc, mContentResolver);
530                        } else {
531                            // This implies a connection issue that we can't handle
532                            throw new IOException();
533                        }
534                    } else {
535                        // It shouldn't be possible for EAS server to send chunked data here
536                        throw new IOException();
537                    }
538                } else if (code == HttpURLConnection.HTTP_UNAUTHORIZED ||
539                        code == HttpURLConnection.HTTP_FORBIDDEN) {
540                    mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE;
541                    userLog("Authorization error during Ping: " + code);
542                    throw new IOException();
543                }
544            } else if (pushCount > 0) {
545                // If we want to Ping, but can't just yet, wait 10 seconds and try again
546                userLog("pingLoop waiting for " + (pushCount - canPushCount) + " box(es)");
547                sleep(10*SECS);
548            } else {
549                // We've got nothing to do, so let's hang out for a while
550                sleep(10*MINS);
551            }
552        }
553    }
554
555    void sleep(long ms) {
556        try {
557            Thread.sleep(ms);
558        } catch (InterruptedException e) {
559            // Doesn't matter whether we stop early; it's the thought that counts
560        }
561    }
562
563    void parsePingResult(HttpURLConnection uc, ContentResolver cr)
564        throws IOException, StaleFolderListException {
565        EasPingParser pp = new EasPingParser(uc.getInputStream(), this);
566        if (pp.parse()) {
567            // True indicates some mailboxes need syncing...
568            // syncList has the serverId's of the mailboxes...
569            mBindArguments[0] = Long.toString(mAccount.mId);
570            ArrayList<String> syncList = pp.getSyncList();
571            for (String serverId: syncList) {
572                mBindArguments[1] = serverId;
573                Cursor c = cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
574                        WHERE_ACCOUNT_KEY_AND_SERVER_ID, mBindArguments, null);
575                try {
576                    if (c.moveToFirst()) {
577                        SyncManager.startManualSync(c.getLong(Mailbox.CONTENT_ID_COLUMN), null);
578                    }
579                } finally {
580                    c.close();
581                }
582            }
583        }
584    }
585
586    ByteArrayInputStream readResponse(HttpURLConnection uc) throws IOException {
587        String encoding = uc.getHeaderField("Transfer-Encoding");
588        if (encoding == null) {
589            int len = uc.getHeaderFieldInt("Content-Length", 0);
590            if (len > 0) {
591                InputStream in = uc.getInputStream();
592                byte[] bytes = new byte[len];
593                int remain = len;
594                int offs = 0;
595                while (remain > 0) {
596                    int read = in.read(bytes, offs, remain);
597                    remain -= read;
598                    offs += read;
599                }
600                return new ByteArrayInputStream(bytes);
601            }
602        } else if (encoding.equalsIgnoreCase("chunked")) {
603            // TODO We don't handle this yet
604            return null;
605        }
606        return null;
607    }
608
609    String readResponseString(HttpURLConnection uc) throws IOException {
610        String encoding = uc.getHeaderField("Transfer-Encoding");
611        if (encoding == null) {
612            int len = uc.getHeaderFieldInt("Content-Length", 0);
613            if (len > 0) {
614                InputStream in = uc.getInputStream();
615                byte[] bytes = new byte[len];
616                int remain = len;
617                int offs = 0;
618                while (remain > 0) {
619                    int read = in.read(bytes, offs, remain);
620                    remain -= read;
621                    offs += read;
622                }
623                return new String(bytes);
624            }
625        } else if (encoding.equalsIgnoreCase("chunked")) {
626            // TODO We don't handle this yet
627            return null;
628        }
629        return null;
630    }
631
632    /**
633     * EAS requires a unique device id, so that sync is possible from a variety of different
634     * devices (e.g. the syncKey is specific to a device)  If we're on an emulator or some other
635     * device that doesn't provide one, we can create it as droid<n> where <n> is system time.
636     * This would work on a real device as well, but it would be better to use the "real" id if
637     * it's available
638     */
639    private String getSimulatedDeviceId() {
640        try {
641            File f = mContext.getFileStreamPath("deviceName");
642            BufferedReader rdr = null;
643            String id;
644            if (f.exists() && f.canRead()) {
645                rdr = new BufferedReader(new FileReader(f));
646                id = rdr.readLine();
647                rdr.close();
648                return id;
649            } else if (f.createNewFile()) {
650                BufferedWriter w = new BufferedWriter(new FileWriter(f));
651                id = "droid" + System.currentTimeMillis();
652                w.write(id);
653                w.close();
654            }
655        } catch (FileNotFoundException e) {
656            // We'll just use the default below
657        } catch (IOException e) {
658            // We'll just use the default below
659        }
660        return "droid0";
661    }
662
663    /**
664     * Common code to sync E+PIM data
665     *
666     * @param target, an EasMailbox, EasContacts, or EasCalendar object
667     */
668    public void sync(EasSyncAdapter target) throws IOException {
669        mTarget = target;
670        Mailbox mailbox = target.mMailbox;
671
672        boolean moreAvailable = true;
673        while (!mStop && moreAvailable) {
674            runAwake();
675            waitForConnectivity();
676
677            while (true) {
678                PartRequest req = null;
679                synchronized (mPartRequests) {
680                    if (mPartRequests.isEmpty()) {
681                        break;
682                    } else {
683                        req = mPartRequests.get(0);
684                    }
685                }
686                getAttachment(req, true);
687                synchronized(mPartRequests) {
688                    mPartRequests.remove(req);
689                }
690            }
691
692            EasSerializer s = new EasSerializer();
693            if (mailbox.mSyncKey == null) {
694                userLog("Mailbox syncKey RESET");
695                mailbox.mSyncKey = "0";
696                mailbox.mSyncFrequency = Account.CHECK_INTERVAL_PUSH;
697            }
698            String className = target.getCollectionName();
699            userLog("Sending " + className + " syncKey: " + mailbox.mSyncKey);
700            s.start("Sync")
701                .start("Collections")
702                .start("Collection")
703                .data("Class", className)
704                .data("SyncKey", mailbox.mSyncKey)
705                .data("CollectionId", mailbox.mServerId)
706                .tag("DeletesAsMoves");
707
708            // EAS doesn't like GetChanges if the syncKey is "0"; not documented
709            if (!mailbox.mSyncKey.equals("0")) {
710                s.tag("GetChanges");
711            }
712            s.data("WindowSize", WINDOW_SIZE);
713            boolean options = false;
714            if (!className.equals("Contacts")) {
715                // Set the lookback appropriately (EAS calls this a "filter")
716                String filter = Eas.FILTER_1_WEEK;
717                switch (mAccount.mSyncLookback) {
718                    case com.android.email.Account.SYNC_WINDOW_1_DAY: {
719                        filter = Eas.FILTER_1_DAY;
720                        break;
721                    }
722                    case com.android.email.Account.SYNC_WINDOW_3_DAYS: {
723                        filter = Eas.FILTER_3_DAYS;
724                        break;
725                    }
726                    case com.android.email.Account.SYNC_WINDOW_1_WEEK: {
727                        filter = Eas.FILTER_1_WEEK;
728                        break;
729                    }
730                    case com.android.email.Account.SYNC_WINDOW_2_WEEKS: {
731                        filter = Eas.FILTER_2_WEEKS;
732                        break;
733                    }
734                    case com.android.email.Account.SYNC_WINDOW_1_MONTH: {
735                        filter = Eas.FILTER_1_MONTH;
736                        break;
737                    }
738                    case com.android.email.Account.SYNC_WINDOW_ALL: {
739                        filter = Eas.FILTER_ALL;
740                        break;
741                    }
742                }
743                s.start("Options").data("FilterType", filter);
744                if (mProtocolVersionDouble < 12.0) {
745                    s.data("Truncation", "7");
746                }
747                options = true;
748            }
749            if (mProtocolVersionDouble >= 12.0) {
750                if (!options) {
751                    options = true;
752                    s.start("Options");
753                }
754                s.start("BodyPreference")
755                    .data("BodyPreferenceType", Eas.BODY_PREFERENCE_HTML)
756                    .data("BodyPreferenceTruncationSize", Eas.DEFAULT_BODY_TRUNCATION_SIZE)
757                    .end("BodyPreference");
758            }
759            if (options) {
760                s.end("Options");
761            }
762
763            // Send our changes up to the server
764            target.sendLocalChanges(s, this);
765
766            s.end("Collection").end("Collections").end("Sync").end();
767            HttpURLConnection uc = sendEASPostCommand("Sync", s.toString());
768            int code = uc.getResponseCode();
769            if (code == HttpURLConnection.HTTP_OK) {
770                ByteArrayInputStream is = readResponse(uc);
771                if (is != null) {
772                    moreAvailable = target.parse(is, this);
773                    target.cleanup(this);
774                }
775            } else {
776                userLog("Sync response error: " + code);
777                if (code == HttpURLConnection.HTTP_UNAUTHORIZED ||
778                        code == HttpURLConnection.HTTP_FORBIDDEN) {
779                    mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE;
780                }
781                return;
782            }
783        }
784    }
785
786    /* (non-Javadoc)
787     * @see java.lang.Runnable#run()
788     */
789    public void run() {
790        mThread = Thread.currentThread();
791        TAG = mThread.getName();
792        mDeviceId = android.provider.Settings.Secure.getString(mContext.getContentResolver(),
793                android.provider.Settings.Secure.ANDROID_ID);
794        // Generate a device id if we don't have one
795        if (mDeviceId == null) {
796            mDeviceId = getSimulatedDeviceId();
797        }
798        HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv);
799        mHostAddress = ha.mAddress;
800        mUserName = ha.mLogin;
801        mPassword = ha.mPassword;
802
803        // Make sure account and mailbox are always the latest from the database
804        mAccount = Account.restoreAccountWithId(mContext, mAccount.mId);
805        mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId);
806        try {
807            if (mMailbox.mServerId.equals(Eas.ACCOUNT_MAILBOX)) {
808                runMain();
809            } else {
810                EasSyncAdapter target;
811                mAccount = Account.restoreAccountWithId(mContext, mAccount.mId);
812                mProtocolVersion = mAccount.mProtocolVersion;
813                mProtocolVersionDouble = Double.parseDouble(mProtocolVersion);
814                if (mMailbox.mType == Mailbox.TYPE_CONTACTS)
815                    target = new EasContactsSyncAdapter(mMailbox);
816                else {
817                    target = new EasEmailSyncAdapter(mMailbox);
818                }
819                // We loop here because someone might have put a request in while we were syncing
820                // and we've missed that opportunity...
821                do {
822                    if (mRequestTime != 0) {
823                        userLog("Looping for user request...");
824                        mRequestTime = 0;
825                    }
826                    sync(target);
827                } while (mRequestTime != 0);
828            }
829            mExitStatus = EXIT_DONE;
830        } catch (IOException e) {
831            userLog("Caught IOException");
832            mExitStatus = EXIT_IO_ERROR;
833        } catch (Exception e) {
834            e.printStackTrace();
835        } finally {
836            userLog(mMailbox.mDisplayName + ": sync finished");
837            SyncManager.done(this);
838        }
839    }
840}
841