• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008-2009 Marc Blank
3  * Licensed to The Android Open Source Project.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.exchange;
19 
20 import com.android.email.SecurityPolicy;
21 import com.android.email.Utility;
22 import com.android.email.SecurityPolicy.PolicySet;
23 import com.android.email.mail.Address;
24 import com.android.email.mail.AuthenticationFailedException;
25 import com.android.email.mail.MeetingInfo;
26 import com.android.email.mail.MessagingException;
27 import com.android.email.mail.PackedString;
28 import com.android.email.provider.EmailContent.Account;
29 import com.android.email.provider.EmailContent.AccountColumns;
30 import com.android.email.provider.EmailContent.Attachment;
31 import com.android.email.provider.EmailContent.AttachmentColumns;
32 import com.android.email.provider.EmailContent.HostAuth;
33 import com.android.email.provider.EmailContent.Mailbox;
34 import com.android.email.provider.EmailContent.MailboxColumns;
35 import com.android.email.provider.EmailContent.Message;
36 import com.android.email.service.EmailServiceConstants;
37 import com.android.email.service.EmailServiceProxy;
38 import com.android.email.service.EmailServiceStatus;
39 import com.android.exchange.adapter.AbstractSyncAdapter;
40 import com.android.exchange.adapter.AccountSyncAdapter;
41 import com.android.exchange.adapter.CalendarSyncAdapter;
42 import com.android.exchange.adapter.ContactsSyncAdapter;
43 import com.android.exchange.adapter.EmailSyncAdapter;
44 import com.android.exchange.adapter.FolderSyncParser;
45 import com.android.exchange.adapter.GalParser;
46 import com.android.exchange.adapter.MeetingResponseParser;
47 import com.android.exchange.adapter.PingParser;
48 import com.android.exchange.adapter.ProvisionParser;
49 import com.android.exchange.adapter.Serializer;
50 import com.android.exchange.adapter.Tags;
51 import com.android.exchange.adapter.Parser.EasParserException;
52 import com.android.exchange.provider.GalResult;
53 import com.android.exchange.utility.CalendarUtilities;
54 
55 import org.apache.http.Header;
56 import org.apache.http.HttpEntity;
57 import org.apache.http.HttpResponse;
58 import org.apache.http.HttpStatus;
59 import org.apache.http.client.HttpClient;
60 import org.apache.http.client.methods.HttpOptions;
61 import org.apache.http.client.methods.HttpPost;
62 import org.apache.http.client.methods.HttpRequestBase;
63 import org.apache.http.conn.ClientConnectionManager;
64 import org.apache.http.entity.ByteArrayEntity;
65 import org.apache.http.entity.StringEntity;
66 import org.apache.http.impl.client.DefaultHttpClient;
67 import org.apache.http.params.BasicHttpParams;
68 import org.apache.http.params.HttpConnectionParams;
69 import org.apache.http.params.HttpParams;
70 import org.xmlpull.v1.XmlPullParser;
71 import org.xmlpull.v1.XmlPullParserException;
72 import org.xmlpull.v1.XmlPullParserFactory;
73 import org.xmlpull.v1.XmlSerializer;
74 
75 import android.content.ContentResolver;
76 import android.content.ContentUris;
77 import android.content.ContentValues;
78 import android.content.Context;
79 import android.content.Entity;
80 import android.database.Cursor;
81 import android.os.Bundle;
82 import android.os.RemoteException;
83 import android.os.SystemClock;
84 import android.provider.Calendar.Attendees;
85 import android.provider.Calendar.Events;
86 import android.text.TextUtils;
87 import android.util.Base64;
88 import android.util.Log;
89 import android.util.Xml;
90 
91 import java.io.ByteArrayOutputStream;
92 import java.io.File;
93 import java.io.FileOutputStream;
94 import java.io.IOException;
95 import java.io.InputStream;
96 import java.lang.Thread.State;
97 import java.net.URI;
98 import java.net.URLEncoder;
99 import java.security.cert.CertificateException;
100 import java.util.ArrayList;
101 import java.util.HashMap;
102 
103 public class EasSyncService extends AbstractSyncService {
104     // DO NOT CHECK IN SET TO TRUE
105     public static final boolean DEBUG_GAL_SERVICE = false;
106 
107     private static final String EMAIL_WINDOW_SIZE = "5";
108     public static final String PIM_WINDOW_SIZE = "4";
109     private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID =
110         MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SERVER_ID + "=?";
111     private static final String WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING =
112         MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL +
113         '=' + Mailbox.CHECK_INTERVAL_PING;
114     private static final String AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX = " AND " +
115         MailboxColumns.SYNC_INTERVAL + " IN (" + Mailbox.CHECK_INTERVAL_PING +
116         ',' + Mailbox.CHECK_INTERVAL_PUSH + ") AND " + MailboxColumns.TYPE + "!=\"" +
117         Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + '\"';
118     private static final String WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX =
119         MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL +
120         '=' + Mailbox.CHECK_INTERVAL_PUSH_HOLD;
121     static private final int CHUNK_SIZE = 16*1024;
122 
123     static private final String PING_COMMAND = "Ping";
124     // Command timeout is the the time allowed for reading data from an open connection before an
125     // IOException is thrown.  After a small added allowance, our watchdog alarm goes off (allowing
126     // us to detect a silently dropped connection).  The allowance is defined below.
127     static private final int COMMAND_TIMEOUT = 30*SECONDS;
128     // Connection timeout is the time given to connect to the server before reporting an IOException
129     static private final int CONNECTION_TIMEOUT = 20*SECONDS;
130     // The extra time allowed beyond the COMMAND_TIMEOUT before which our watchdog alarm triggers
131     static private final int WATCHDOG_TIMEOUT_ALLOWANCE = 30*SECONDS;
132 
133     // The amount of time the account mailbox will sleep if there are no pingable mailboxes
134     // This could happen if the sync time is set to "never"; we always want to check in from time
135     // to time, however, for folder list/policy changes
136     static private final int ACCOUNT_MAILBOX_SLEEP_TIME = 20*MINUTES;
137     static private final String ACCOUNT_MAILBOX_SLEEP_TEXT =
138         "Account mailbox sleeping for " + (ACCOUNT_MAILBOX_SLEEP_TIME / MINUTES) + "m";
139 
140     static private final String AUTO_DISCOVER_SCHEMA_PREFIX =
141         "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/";
142     static private final String AUTO_DISCOVER_PAGE = "/autodiscover/autodiscover.xml";
143     static private final int AUTO_DISCOVER_REDIRECT_CODE = 451;
144 
145     static public final String EAS_12_POLICY_TYPE = "MS-EAS-Provisioning-WBXML";
146     static public final String EAS_2_POLICY_TYPE = "MS-WAP-Provisioning-XML";
147 
148     /**
149      * We start with an 8 minute timeout, and increase/decrease by 3 minutes at a time.  There's
150      * no point having a timeout shorter than 5 minutes, I think; at that point, we can just let
151      * the ping exception out.  The maximum I use is 17 minutes, which is really an empirical
152      * choice; too long and we risk silent connection loss and loss of push for that period.  Too
153      * short and we lose efficiency/battery life.
154      *
155      * If we ever have to drop the ping timeout, we'll never increase it again.  There's no point
156      * going into hysteresis; the NAT timeout isn't going to change without a change in connection,
157      * which will cause the sync service to be restarted at the starting heartbeat and going through
158      * the process again.
159      */
160     static private final int PING_MINUTES = 60; // in seconds
161     static private final int PING_FUDGE_LOW = 10;
162     static private final int PING_STARTING_HEARTBEAT = (8*PING_MINUTES)-PING_FUDGE_LOW;
163     static private final int PING_HEARTBEAT_INCREMENT = 3*PING_MINUTES;
164 
165     // Maximum number of times we'll allow a sync to "loop" with MoreAvailable true before
166     // forcing it to stop.  This number has been determined empirically.
167     static private final int MAX_LOOPING_COUNT = 100;
168 
169     static private final int PROTOCOL_PING_STATUS_COMPLETED = 1;
170 
171     // The amount of time we allow for a thread to release its post lock after receiving an alert
172     static private final int POST_LOCK_TIMEOUT = 10*SECONDS;
173 
174     // Fallbacks (in minutes) for ping loop failures
175     static private final int MAX_PING_FAILURES = 1;
176     static private final int PING_FALLBACK_INBOX = 5;
177     static private final int PING_FALLBACK_PIM = 25;
178 
179     // MSFT's custom HTTP result code indicating the need to provision
180     static private final int HTTP_NEED_PROVISIONING = 449;
181 
182     // The EAS protocol Provision status for "we implement all of the policies"
183     static private final String PROVISION_STATUS_OK = "1";
184     // The EAS protocol Provision status meaning "we partially implement the policies"
185     static private final String PROVISION_STATUS_PARTIAL = "2";
186 
187     // Reasonable default
188     public String mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION;
189     public Double mProtocolVersionDouble;
190     protected String mDeviceId = null;
191     /*package*/ String mDeviceType = "Android";
192     /*package*/ String mAuthString = null;
193     private String mCmdString = null;
194     public String mHostAddress;
195     public String mUserName;
196     public String mPassword;
197     private boolean mSsl = true;
198     private boolean mTrustSsl = false;
199     public ContentResolver mContentResolver;
200     private String[] mBindArguments = new String[2];
201     private ArrayList<String> mPingChangeList;
202     // The HttpPost in progress
203     private volatile HttpPost mPendingPost = null;
204     // Our heartbeat when we are waiting for ping boxes to be ready
205     /*package*/ int mPingForceHeartbeat = 2*PING_MINUTES;
206     // The minimum heartbeat we will send
207     /*package*/ int mPingMinHeartbeat = (5*PING_MINUTES)-PING_FUDGE_LOW;
208     // The maximum heartbeat we will send
209     /*package*/ int mPingMaxHeartbeat = (17*PING_MINUTES)-PING_FUDGE_LOW;
210     // The ping time (in seconds)
211     /*package*/ int mPingHeartbeat = PING_STARTING_HEARTBEAT;
212     // The longest successful ping heartbeat
213     private int mPingHighWaterMark = 0;
214     // Whether we've ever lowered the heartbeat
215     /*package*/ boolean mPingHeartbeatDropped = false;
216     // Whether a POST was aborted due to alarm (watchdog alarm)
217     private boolean mPostAborted = false;
218     // Whether a POST was aborted due to reset
219     private boolean mPostReset = false;
220     // Whether or not the sync service is valid (usable)
221     public boolean mIsValid = true;
222 
EasSyncService(Context _context, Mailbox _mailbox)223     public EasSyncService(Context _context, Mailbox _mailbox) {
224         super(_context, _mailbox);
225         mContentResolver = _context.getContentResolver();
226         if (mAccount == null) {
227             mIsValid = false;
228             return;
229         }
230         HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv);
231         if (ha == null) {
232             mIsValid = false;
233             return;
234         }
235         mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0;
236         mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL_CERTIFICATES) != 0;
237     }
238 
EasSyncService(String prefix)239     private EasSyncService(String prefix) {
240         super(prefix);
241     }
242 
EasSyncService()243     public EasSyncService() {
244         this("EAS Validation");
245     }
246 
247     @Override
248     /**
249      * Try to wake up a sync thread that is waiting on an HttpClient POST and has waited past its
250      * socket timeout without having thrown an Exception
251      *
252      * @return true if the POST was successfully stopped; false if we've failed and interrupted
253      * the thread
254      */
alarm()255     public boolean alarm() {
256         HttpPost post;
257         if (mThread == null) return true;
258         String threadName = mThread.getName();
259 
260         // Synchronize here so that we are guaranteed to have valid mPendingPost and mPostLock
261         // executePostWithTimeout (which executes the HttpPost) also uses this lock
262         synchronized(getSynchronizer()) {
263             // Get a reference to the current post lock
264             post = mPendingPost;
265             if (post != null) {
266                 if (Eas.USER_LOG) {
267                     URI uri = post.getURI();
268                     if (uri != null) {
269                         String query = uri.getQuery();
270                         if (query == null) {
271                             query = "POST";
272                         }
273                         userLog(threadName, ": Alert, aborting ", query);
274                     } else {
275                         userLog(threadName, ": Alert, no URI?");
276                     }
277                 }
278                 // Abort the POST
279                 mPostAborted = true;
280                 post.abort();
281             } else {
282                 // If there's no POST, we're done
283                 userLog("Alert, no pending POST");
284                 return true;
285             }
286         }
287 
288         // Wait for the POST to finish
289         try {
290             Thread.sleep(POST_LOCK_TIMEOUT);
291         } catch (InterruptedException e) {
292         }
293 
294         State s = mThread.getState();
295         if (Eas.USER_LOG) {
296             userLog(threadName + ": State = " + s.name());
297         }
298 
299         synchronized (getSynchronizer()) {
300             // If the thread is still hanging around and the same post is pending, let's try to
301             // stop the thread with an interrupt.
302             if ((s != State.TERMINATED) && (mPendingPost != null) && (mPendingPost == post)) {
303                 mStop = true;
304                 mThread.interrupt();
305                 userLog("Interrupting...");
306                 // Let the caller know we had to interrupt the thread
307                 return false;
308             }
309         }
310         // Let the caller know that the alarm was handled normally
311         return true;
312     }
313 
314     @Override
reset()315     public void reset() {
316         synchronized(getSynchronizer()) {
317             if (mPendingPost != null) {
318                 URI uri = mPendingPost.getURI();
319                 if (uri != null) {
320                     String query = uri.getQuery();
321                     if (query.startsWith("Cmd=Ping")) {
322                         userLog("Reset, aborting Ping");
323                         mPostReset = true;
324                         mPendingPost.abort();
325                     }
326                 }
327             }
328         }
329     }
330 
331     @Override
stop()332     public void stop() {
333         mStop = true;
334         synchronized(getSynchronizer()) {
335             if (mPendingPost != null) {
336                 mPendingPost.abort();
337             }
338         }
339     }
340 
341     /**
342      * Determine whether an HTTP code represents an authentication error
343      * @param code the HTTP code returned by the server
344      * @return whether or not the code represents an authentication error
345      */
isAuthError(int code)346     protected boolean isAuthError(int code) {
347         return (code == HttpStatus.SC_UNAUTHORIZED) || (code == HttpStatus.SC_FORBIDDEN);
348     }
349 
350     /**
351      * Determine whether an HTTP code represents a provisioning error
352      * @param code the HTTP code returned by the server
353      * @return whether or not the code represents an provisioning error
354      */
isProvisionError(int code)355     protected boolean isProvisionError(int code) {
356         return (code == HTTP_NEED_PROVISIONING) || (code == HttpStatus.SC_FORBIDDEN);
357     }
358 
setupProtocolVersion(EasSyncService service, Header versionHeader)359     private void setupProtocolVersion(EasSyncService service, Header versionHeader)
360             throws MessagingException {
361         // The string is a comma separated list of EAS versions in ascending order
362         // e.g. 1.0,2.0,2.5,12.0,12.1
363         String supportedVersions = versionHeader.getValue();
364         userLog("Server supports versions: ", supportedVersions);
365         String[] supportedVersionsArray = supportedVersions.split(",");
366         String ourVersion = null;
367         // Find the most recent version we support
368         for (String version: supportedVersionsArray) {
369             if (version.equals(Eas.SUPPORTED_PROTOCOL_EX2003) ||
370                     version.equals(Eas.SUPPORTED_PROTOCOL_EX2007)) {
371                 ourVersion = version;
372             }
373         }
374         // If we don't support any of the servers supported versions, throw an exception here
375         // This will cause validation to fail
376         if (ourVersion == null) {
377             Log.w(TAG, "No supported EAS versions: " + supportedVersions);
378             throw new MessagingException(MessagingException.PROTOCOL_VERSION_UNSUPPORTED);
379         } else {
380             service.mProtocolVersion = ourVersion;
381             service.mProtocolVersionDouble = Double.parseDouble(ourVersion);
382             if (service.mAccount != null) {
383                 service.mAccount.mProtocolVersion = ourVersion;
384             }
385         }
386     }
387 
388     @Override
validateAccount(String hostAddress, String userName, String password, int port, boolean ssl, boolean trustCertificates, Context context)389     public void validateAccount(String hostAddress, String userName, String password, int port,
390             boolean ssl, boolean trustCertificates, Context context) throws MessagingException {
391         try {
392             userLog("Testing EAS: ", hostAddress, ", ", userName, ", ssl = ", ssl ? "1" : "0");
393             EasSyncService svc = new EasSyncService("%TestAccount%");
394             svc.mContext = context;
395             svc.mHostAddress = hostAddress;
396             svc.mUserName = userName;
397             svc.mPassword = password;
398             svc.mSsl = ssl;
399             svc.mTrustSsl = trustCertificates;
400             // We mustn't use the "real" device id or we'll screw up current accounts
401             // Any string will do, but we'll go for "validate"
402             svc.mDeviceId = "validate";
403             HttpResponse resp = svc.sendHttpClientOptions();
404             int code = resp.getStatusLine().getStatusCode();
405             userLog("Validation (OPTIONS) response: " + code);
406             if (code == HttpStatus.SC_OK) {
407                 // No exception means successful validation
408                 Header commands = resp.getFirstHeader("MS-ASProtocolCommands");
409                 Header versions = resp.getFirstHeader("ms-asprotocolversions");
410                 if (commands == null || versions == null) {
411                     userLog("OPTIONS response without commands or versions; reporting I/O error");
412                     throw new MessagingException(MessagingException.IOERROR);
413                 }
414 
415                 // Make sure we've got the right protocol version set up
416                 setupProtocolVersion(svc, versions);
417 
418                 // Run second test here for provisioning failures...
419                 Serializer s = new Serializer();
420                 userLog("Validate: try folder sync");
421                 s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY).text("0")
422                     .end().end().done();
423                 resp = svc.sendHttpClientPost("FolderSync", s.toByteArray());
424                 code = resp.getStatusLine().getStatusCode();
425                 // We'll get one of the following responses if policies are required by the server
426                 if (code == HttpStatus.SC_FORBIDDEN || code == HTTP_NEED_PROVISIONING) {
427                     // Get the policies and see if we are able to support them
428                     userLog("Validate: provisioning required");
429                     if (svc.canProvision() != null) {
430                         // If so, send the advisory Exception (the account may be created later)
431                         userLog("Validate: provisioning is possible");
432                         throw new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
433                     } else
434                         userLog("Validate: provisioning not possible");
435                         // If not, send the unsupported Exception (the account won't be created)
436                         throw new MessagingException(
437                                 MessagingException.SECURITY_POLICIES_UNSUPPORTED);
438                 } else if (code == HttpStatus.SC_NOT_FOUND) {
439                     userLog("Wrong address or bad protocol version");
440                     // We get a 404 from OWA addresses (which are NOT EAS addresses)
441                     throw new MessagingException(MessagingException.PROTOCOL_VERSION_UNSUPPORTED);
442                 } else if (code != HttpStatus.SC_OK) {
443                     // Fail generically with anything other than success
444                     userLog("Unexpected response for FolderSync: ", code);
445                     throw new MessagingException(MessagingException.UNSPECIFIED_EXCEPTION);
446                 }
447                 userLog("Validation successful");
448                 return;
449             }
450             if (isAuthError(code)) {
451                 userLog("Authentication failed");
452                 throw new AuthenticationFailedException("Validation failed");
453             } else {
454                 // TODO Need to catch other kinds of errors (e.g. policy) For now, report the code.
455                 userLog("Validation failed, reporting I/O error: ", code);
456                 throw new MessagingException(MessagingException.IOERROR);
457             }
458         } catch (IOException e) {
459             Throwable cause = e.getCause();
460             if (cause != null && cause instanceof CertificateException) {
461                 userLog("CertificateException caught: ", e.getMessage());
462                 throw new MessagingException(MessagingException.GENERAL_SECURITY);
463             }
464             userLog("IOException caught: ", e.getMessage());
465             throw new MessagingException(MessagingException.IOERROR);
466         }
467 
468     }
469 
470     /**
471      * Gets the redirect location from the HTTP headers and uses that to modify the HttpPost so that
472      * it can be reused
473      *
474      * @param resp the HttpResponse that indicates a redirect (451)
475      * @param post the HttpPost that was originally sent to the server
476      * @return the HttpPost, updated with the redirect location
477      */
getRedirect(HttpResponse resp, HttpPost post)478     private HttpPost getRedirect(HttpResponse resp, HttpPost post) {
479         Header locHeader = resp.getFirstHeader("X-MS-Location");
480         if (locHeader != null) {
481             String loc = locHeader.getValue();
482             // If we've gotten one and it shows signs of looking like an address, we try
483             // sending our request there
484             if (loc != null && loc.startsWith("http")) {
485                 post.setURI(URI.create(loc));
486                 return post;
487             }
488         }
489         return null;
490     }
491 
492     /**
493      * Send the POST command to the autodiscover server, handling a redirect, if necessary, and
494      * return the HttpResponse.  If we get a 401 (unauthorized) error and we're using the
495      * full email address, try the bare user name instead (e.g. foo instead of foo@bar.com)
496      *
497      * @param client the HttpClient to be used for the request
498      * @param post the HttpPost we're going to send
499      * @param canRetry whether we can retry using the bare name on an authentication failure (401)
500      * @return an HttpResponse from the original or redirect server
501      * @throws IOException on any IOException within the HttpClient code
502      * @throws MessagingException
503      */
postAutodiscover(HttpClient client, HttpPost post, boolean canRetry)504     private HttpResponse postAutodiscover(HttpClient client, HttpPost post, boolean canRetry)
505             throws IOException, MessagingException {
506         userLog("Posting autodiscover to: " + post.getURI());
507         HttpResponse resp = executePostWithTimeout(client, post, COMMAND_TIMEOUT);
508         int code = resp.getStatusLine().getStatusCode();
509         // On a redirect, try the new location
510         if (code == AUTO_DISCOVER_REDIRECT_CODE) {
511             post = getRedirect(resp, post);
512             if (post != null) {
513                 userLog("Posting autodiscover to redirect: " + post.getURI());
514                 return executePostWithTimeout(client, post, COMMAND_TIMEOUT);
515             }
516         // 401 (Unauthorized) is for true auth errors when used in Autodiscover
517         } else if (code == HttpStatus.SC_UNAUTHORIZED) {
518             if (canRetry && mUserName.contains("@")) {
519                 // Try again using the bare user name
520                 int atSignIndex = mUserName.indexOf('@');
521                 mUserName = mUserName.substring(0, atSignIndex);
522                 cacheAuthAndCmdString();
523                 userLog("401 received; trying username: ", mUserName);
524                 // Recreate the basic authentication string and reset the header
525                 post.removeHeaders("Authorization");
526                 post.setHeader("Authorization", mAuthString);
527                 return postAutodiscover(client, post, false);
528             }
529             throw new MessagingException(MessagingException.AUTHENTICATION_FAILED);
530         // 403 (and others) we'll just punt on
531         } else if (code != HttpStatus.SC_OK) {
532             // We'll try the next address if this doesn't work
533             userLog("Code: " + code + ", throwing IOException");
534             throw new IOException();
535         }
536         return resp;
537     }
538 
539     /**
540      * Use the Exchange 2007 AutoDiscover feature to try to retrieve server information using
541      * only an email address and the password
542      *
543      * @param userName the user's email address
544      * @param password the user's password
545      * @return a HostAuth ready to be saved in an Account or null (failure)
546      */
tryAutodiscover(String userName, String password)547     public Bundle tryAutodiscover(String userName, String password) throws RemoteException {
548         XmlSerializer s = Xml.newSerializer();
549         ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
550         HostAuth hostAuth = new HostAuth();
551         Bundle bundle = new Bundle();
552         bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
553                 MessagingException.NO_ERROR);
554         try {
555             // Build the XML document that's sent to the autodiscover server(s)
556             s.setOutput(os, "UTF-8");
557             s.startDocument("UTF-8", false);
558             s.startTag(null, "Autodiscover");
559             s.attribute(null, "xmlns", AUTO_DISCOVER_SCHEMA_PREFIX + "requestschema/2006");
560             s.startTag(null, "Request");
561             s.startTag(null, "EMailAddress").text(userName).endTag(null, "EMailAddress");
562             s.startTag(null, "AcceptableResponseSchema");
563             s.text(AUTO_DISCOVER_SCHEMA_PREFIX + "responseschema/2006");
564             s.endTag(null, "AcceptableResponseSchema");
565             s.endTag(null, "Request");
566             s.endTag(null, "Autodiscover");
567             s.endDocument();
568             String req = os.toString();
569 
570             // Initialize the user name and password
571             mUserName = userName;
572             mPassword = password;
573             // Make sure the authentication string is recreated and cached
574             cacheAuthAndCmdString();
575 
576             // Split out the domain name
577             int amp = userName.indexOf('@');
578             // The UI ensures that userName is a valid email address
579             if (amp < 0) {
580                 throw new RemoteException();
581             }
582             String domain = userName.substring(amp + 1);
583 
584             // There are up to four attempts here; the two URLs that we're supposed to try per the
585             // specification, and up to one redirect for each (handled in postAutodiscover)
586             // Note: The expectation is that, of these four attempts, only a single server will
587             // actually be identified as the autodiscover server.  For the identified server,
588             // we may also try a 2nd connection with a different format (bare name).
589 
590             // Try the domain first and see if we can get a response
591             HttpPost post = new HttpPost("https://" + domain + AUTO_DISCOVER_PAGE);
592             setHeaders(post, false);
593             post.setHeader("Content-Type", "text/xml");
594             post.setEntity(new StringEntity(req));
595             HttpClient client = getHttpClient(COMMAND_TIMEOUT);
596             HttpResponse resp;
597             try {
598                 resp = postAutodiscover(client, post, true /*canRetry*/);
599             } catch (IOException e1) {
600                 userLog("IOException in autodiscover; trying alternate address");
601                 // We catch the IOException here because we have an alternate address to try
602                 post.setURI(URI.create("https://autodiscover." + domain + AUTO_DISCOVER_PAGE));
603                 // If we fail here, we're out of options, so we let the outer try catch the
604                 // IOException and return null
605                 resp = postAutodiscover(client, post, true /*canRetry*/);
606             }
607 
608             // Get the "final" code; if it's not 200, just return null
609             int code = resp.getStatusLine().getStatusCode();
610             userLog("Code: " + code);
611             if (code != HttpStatus.SC_OK) return null;
612 
613             // At this point, we have a 200 response (SC_OK)
614             HttpEntity e = resp.getEntity();
615             InputStream is = e.getContent();
616             try {
617                 // The response to Autodiscover is regular XML (not WBXML)
618                 // If we ever get an error in this process, we'll just punt and return null
619                 XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
620                 XmlPullParser parser = factory.newPullParser();
621                 parser.setInput(is, "UTF-8");
622                 int type = parser.getEventType();
623                 if (type == XmlPullParser.START_DOCUMENT) {
624                     type = parser.next();
625                     if (type == XmlPullParser.START_TAG) {
626                         String name = parser.getName();
627                         if (name.equals("Autodiscover")) {
628                             hostAuth = new HostAuth();
629                             parseAutodiscover(parser, hostAuth);
630                             // On success, we'll have a server address and login
631                             if (hostAuth.mAddress != null) {
632                                 // Fill in the rest of the HostAuth
633                                 // We use the user name and password that were successful during
634                                 // the autodiscover process
635                                 hostAuth.mLogin = mUserName;
636                                 hostAuth.mPassword = mPassword;
637                                 hostAuth.mPort = 443;
638                                 hostAuth.mProtocol = "eas";
639                                 hostAuth.mFlags =
640                                     HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE;
641                                 bundle.putParcelable(
642                                         EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH, hostAuth);
643                             } else {
644                                 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
645                                         MessagingException.UNSPECIFIED_EXCEPTION);
646                             }
647                         }
648                     }
649                 }
650             } catch (XmlPullParserException e1) {
651                 // This would indicate an I/O error of some sort
652                 // We will simply return null and user can configure manually
653             }
654         // There's no reason at all for exceptions to be thrown, and it's ok if so.
655         // We just won't do auto-discover; user can configure manually
656        } catch (IllegalArgumentException e) {
657              bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
658                      MessagingException.UNSPECIFIED_EXCEPTION);
659        } catch (IllegalStateException e) {
660             bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
661                     MessagingException.UNSPECIFIED_EXCEPTION);
662        } catch (IOException e) {
663             userLog("IOException in Autodiscover", e);
664             bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
665                     MessagingException.IOERROR);
666         } catch (MessagingException e) {
667             bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
668                     MessagingException.AUTHENTICATION_FAILED);
669         }
670         return bundle;
671     }
672 
parseServer(XmlPullParser parser, HostAuth hostAuth)673     void parseServer(XmlPullParser parser, HostAuth hostAuth)
674             throws XmlPullParserException, IOException {
675         boolean mobileSync = false;
676         while (true) {
677             int type = parser.next();
678             if (type == XmlPullParser.END_TAG && parser.getName().equals("Server")) {
679                 break;
680             } else if (type == XmlPullParser.START_TAG) {
681                 String name = parser.getName();
682                 if (name.equals("Type")) {
683                     if (parser.nextText().equals("MobileSync")) {
684                         mobileSync = true;
685                     }
686                 } else if (mobileSync && name.equals("Url")) {
687                     String url = parser.nextText().toLowerCase();
688                     // This will look like https://<server address>/Microsoft-Server-ActiveSync
689                     // We need to extract the <server address>
690                     if (url.startsWith("https://") &&
691                             url.endsWith("/microsoft-server-activesync")) {
692                         int lastSlash = url.lastIndexOf('/');
693                         hostAuth.mAddress = url.substring(8, lastSlash);
694                         userLog("Autodiscover, server: " + hostAuth.mAddress);
695                     }
696                 }
697             }
698         }
699     }
700 
parseSettings(XmlPullParser parser, HostAuth hostAuth)701     void parseSettings(XmlPullParser parser, HostAuth hostAuth)
702             throws XmlPullParserException, IOException {
703         while (true) {
704             int type = parser.next();
705             if (type == XmlPullParser.END_TAG && parser.getName().equals("Settings")) {
706                 break;
707             } else if (type == XmlPullParser.START_TAG) {
708                 String name = parser.getName();
709                 if (name.equals("Server")) {
710                     parseServer(parser, hostAuth);
711                 }
712             }
713         }
714     }
715 
parseAction(XmlPullParser parser, HostAuth hostAuth)716     void parseAction(XmlPullParser parser, HostAuth hostAuth)
717             throws XmlPullParserException, IOException {
718         while (true) {
719             int type = parser.next();
720             if (type == XmlPullParser.END_TAG && parser.getName().equals("Action")) {
721                 break;
722             } else if (type == XmlPullParser.START_TAG) {
723                 String name = parser.getName();
724                 if (name.equals("Error")) {
725                     // Should parse the error
726                 } else if (name.equals("Redirect")) {
727                     Log.d(TAG, "Redirect: " + parser.nextText());
728                 } else if (name.equals("Settings")) {
729                     parseSettings(parser, hostAuth);
730                 }
731             }
732         }
733     }
734 
parseUser(XmlPullParser parser, HostAuth hostAuth)735     void parseUser(XmlPullParser parser, HostAuth hostAuth)
736             throws XmlPullParserException, IOException {
737         while (true) {
738             int type = parser.next();
739             if (type == XmlPullParser.END_TAG && parser.getName().equals("User")) {
740                 break;
741             } else if (type == XmlPullParser.START_TAG) {
742                 String name = parser.getName();
743                 if (name.equals("EMailAddress")) {
744                     String addr = parser.nextText();
745                     userLog("Autodiscover, email: " + addr);
746                 } else if (name.equals("DisplayName")) {
747                     String dn = parser.nextText();
748                     userLog("Autodiscover, user: " + dn);
749                 }
750             }
751         }
752     }
753 
parseResponse(XmlPullParser parser, HostAuth hostAuth)754     void parseResponse(XmlPullParser parser, HostAuth hostAuth)
755             throws XmlPullParserException, IOException {
756         while (true) {
757             int type = parser.next();
758             if (type == XmlPullParser.END_TAG && parser.getName().equals("Response")) {
759                 break;
760             } else if (type == XmlPullParser.START_TAG) {
761                 String name = parser.getName();
762                 if (name.equals("User")) {
763                     parseUser(parser, hostAuth);
764                 } else if (name.equals("Action")) {
765                     parseAction(parser, hostAuth);
766                 }
767             }
768         }
769     }
770 
parseAutodiscover(XmlPullParser parser, HostAuth hostAuth)771     void parseAutodiscover(XmlPullParser parser, HostAuth hostAuth)
772             throws XmlPullParserException, IOException {
773         while (true) {
774             int type = parser.nextTag();
775             if (type == XmlPullParser.END_TAG && parser.getName().equals("Autodiscover")) {
776                 break;
777             } else if (type == XmlPullParser.START_TAG && parser.getName().equals("Response")) {
778                 parseResponse(parser, hostAuth);
779             }
780         }
781     }
782 
783     /**
784      * Contact the GAL and obtain a list of matching accounts
785      * @param context caller's context
786      * @param accountId the account Id to search
787      * @param filter the characters entered so far
788      * @return a result record
789      *
790      * TODO: shorter timeout for interactive lookup
791      * TODO: make watchdog actually work (it doesn't understand our service w/Mailbox == 0)
792      * TODO: figure out why sendHttpClientPost() hangs - possibly pool exhaustion
793      */
searchGal(Context context, long accountId, String filter)794     static public GalResult searchGal(Context context, long accountId, String filter) {
795         Account acct = SyncManager.getAccountById(accountId);
796         if (acct != null) {
797             HostAuth ha = HostAuth.restoreHostAuthWithId(context, acct.mHostAuthKeyRecv);
798             EasSyncService svc = new EasSyncService("%GalLookupk%");
799             try {
800                 svc.mContext = context;
801                 svc.mHostAddress = ha.mAddress;
802                 svc.mUserName = ha.mLogin;
803                 svc.mPassword = ha.mPassword;
804                 svc.mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0;
805                 svc.mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL_CERTIFICATES) != 0;
806                 svc.mDeviceId = SyncManager.getDeviceId();
807                 svc.mAccount = acct;
808                 Serializer s = new Serializer();
809                 s.start(Tags.SEARCH_SEARCH).start(Tags.SEARCH_STORE);
810                 s.data(Tags.SEARCH_NAME, "GAL").data(Tags.SEARCH_QUERY, filter);
811                 s.start(Tags.SEARCH_OPTIONS);
812                 s.data(Tags.SEARCH_RANGE, "0-19");  // Return 0..20 results
813                 s.end().end().end().done();
814                 if (DEBUG_GAL_SERVICE) svc.userLog("GAL lookup starting for " + ha.mAddress);
815                 HttpResponse resp = svc.sendHttpClientPost("Search", s.toByteArray());
816                 int code = resp.getStatusLine().getStatusCode();
817                 if (code == HttpStatus.SC_OK) {
818                     InputStream is = resp.getEntity().getContent();
819                     GalParser gp = new GalParser(is, svc);
820                     if (gp.parse()) {
821                         if (DEBUG_GAL_SERVICE) svc.userLog("GAL lookup OK for " + ha.mAddress);
822                         return gp.getGalResult();
823                     } else {
824                         if (DEBUG_GAL_SERVICE) svc.userLog("GAL lookup returned no matches");
825                     }
826                 } else {
827                     svc.userLog("GAL lookup returned " + code);
828                 }
829             } catch (IOException e) {
830                 // GAL is non-critical; we'll just go on
831                 svc.userLog("GAL lookup exception " + e);
832             }
833         }
834         return null;
835     }
836 
doStatusCallback(long messageId, long attachmentId, int status)837     private void doStatusCallback(long messageId, long attachmentId, int status) {
838         try {
839             SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, status, 0);
840         } catch (RemoteException e) {
841             // No danger if the client is no longer around
842         }
843     }
844 
doProgressCallback(long messageId, long attachmentId, int progress)845     private void doProgressCallback(long messageId, long attachmentId, int progress) {
846         try {
847             SyncManager.callback().loadAttachmentStatus(messageId, attachmentId,
848                     EmailServiceStatus.IN_PROGRESS, progress);
849         } catch (RemoteException e) {
850             // No danger if the client is no longer around
851         }
852     }
853 
createUniqueFileInternal(String dir, String filename)854     public File createUniqueFileInternal(String dir, String filename) {
855         File directory;
856         if (dir == null) {
857             directory = mContext.getFilesDir();
858         } else {
859             directory = new File(dir);
860         }
861         if (!directory.exists()) {
862             directory.mkdirs();
863         }
864         File file = new File(directory, filename);
865         if (!file.exists()) {
866             return file;
867         }
868         // Get the extension of the file, if any.
869         int index = filename.lastIndexOf('.');
870         String name = filename;
871         String extension = "";
872         if (index != -1) {
873             name = filename.substring(0, index);
874             extension = filename.substring(index);
875         }
876         for (int i = 2; i < Integer.MAX_VALUE; i++) {
877             file = new File(directory, name + '-' + i + extension);
878             if (!file.exists()) {
879                 return file;
880             }
881         }
882         return null;
883     }
884 
885     /**
886      * Loads an attachment, based on the PartRequest passed in.  The PartRequest is basically our
887      * wrapper for Attachment
888      * @param req the part (attachment) to be retrieved
889      * @throws IOException
890      */
getAttachment(PartRequest req)891     protected void getAttachment(PartRequest req) throws IOException {
892         Attachment att = req.mAttachment;
893         Message msg = Message.restoreMessageWithId(mContext, att.mMessageKey);
894         doProgressCallback(msg.mId, att.mId, 0);
895 
896         String cmd = "GetAttachment&AttachmentName=" + att.mLocation;
897         HttpResponse res = sendHttpClientPost(cmd, null, COMMAND_TIMEOUT);
898 
899         int status = res.getStatusLine().getStatusCode();
900         if (status == HttpStatus.SC_OK) {
901             HttpEntity e = res.getEntity();
902             int len = (int)e.getContentLength();
903             InputStream is = res.getEntity().getContent();
904             File f = (req.mDestination != null)
905                     ? new File(req.mDestination)
906                     : createUniqueFileInternal(req.mDestination, att.mFileName);
907             if (f != null) {
908                 // Ensure that the target directory exists
909                 File destDir = f.getParentFile();
910                 if (!destDir.exists()) {
911                     destDir.mkdirs();
912                 }
913                 FileOutputStream os = new FileOutputStream(f);
914                 // len > 0 means that Content-Length was set in the headers
915                 // len < 0 means "chunked" transfer-encoding
916                 if (len != 0) {
917                     try {
918                         mPendingRequest = req;
919                         byte[] bytes = new byte[CHUNK_SIZE];
920                         int length = len;
921                         // Loop terminates 1) when EOF is reached or 2) if an IOException occurs
922                         // One of these is guaranteed to occur
923                         int totalRead = 0;
924                         userLog("Attachment content-length: ", len);
925                         while (true) {
926                             int read = is.read(bytes, 0, CHUNK_SIZE);
927 
928                             // read < 0 means that EOF was reached
929                             if (read < 0) {
930                                 userLog("Attachment load reached EOF, totalRead: ", totalRead);
931                                 break;
932                             }
933 
934                             // Keep track of how much we've read for progress callback
935                             totalRead += read;
936 
937                             // Write these bytes out
938                             os.write(bytes, 0, read);
939 
940                             // We can't report percentages if this is chunked; by definition, the
941                             // length of incoming data is unknown
942                             if (length > 0) {
943                                 // Belt and suspenders check to prevent runaway reading
944                                 if (totalRead > length) {
945                                     errorLog("totalRead is greater than attachment length?");
946                                     break;
947                                 }
948                                 int pct = (totalRead * 100) / length;
949                                 doProgressCallback(msg.mId, att.mId, pct);
950                             }
951                        }
952                     } finally {
953                         mPendingRequest = null;
954                     }
955                 }
956                 os.flush();
957                 os.close();
958 
959                 // EmailProvider will throw an exception if we try to update an unsaved attachment
960                 if (att.isSaved()) {
961                     String contentUriString = (req.mContentUriString != null)
962                             ? req.mContentUriString
963                             : "file://" + f.getAbsolutePath();
964                     ContentValues cv = new ContentValues();
965                     cv.put(AttachmentColumns.CONTENT_URI, contentUriString);
966                     att.update(mContext, cv);
967                     doStatusCallback(msg.mId, att.mId, EmailServiceStatus.SUCCESS);
968                 }
969             }
970         } else {
971             doStatusCallback(msg.mId, att.mId, EmailServiceStatus.MESSAGE_NOT_FOUND);
972         }
973     }
974 
975     /**
976      * Send an email responding to a Message that has been marked as a meeting request.  The message
977      * will consist a little bit of event information and an iCalendar attachment
978      * @param msg the meeting request email
979      */
sendMeetingResponseMail(Message msg, int response)980     private void sendMeetingResponseMail(Message msg, int response) {
981         // Get the meeting information; we'd better have some...
982         PackedString meetingInfo = new PackedString(msg.mMeetingInfo);
983         if (meetingInfo == null) return;
984 
985         // This will come as "First Last" <box@server.blah>, so we use Address to
986         // parse it into parts; we only need the email address part for the ics file
987         Address[] addrs = Address.parse(meetingInfo.get(MeetingInfo.MEETING_ORGANIZER_EMAIL));
988         // It shouldn't be possible, but handle it anyway
989         if (addrs.length != 1) return;
990         String organizerEmail = addrs[0].getAddress();
991 
992         String dtStamp = meetingInfo.get(MeetingInfo.MEETING_DTSTAMP);
993         String dtStart = meetingInfo.get(MeetingInfo.MEETING_DTSTART);
994         String dtEnd = meetingInfo.get(MeetingInfo.MEETING_DTEND);
995 
996         // What we're doing here is to create an Entity that looks like an Event as it would be
997         // stored by CalendarProvider
998         ContentValues entityValues = new ContentValues();
999         Entity entity = new Entity(entityValues);
1000 
1001         // Fill in times, location, title, and organizer
1002         entityValues.put("DTSTAMP",
1003                 CalendarUtilities.convertEmailDateTimeToCalendarDateTime(dtStamp));
1004         entityValues.put(Events.DTSTART, Utility.parseEmailDateTimeToMillis(dtStart));
1005         entityValues.put(Events.DTEND, Utility.parseEmailDateTimeToMillis(dtEnd));
1006         entityValues.put(Events.EVENT_LOCATION, meetingInfo.get(MeetingInfo.MEETING_LOCATION));
1007         entityValues.put(Events.TITLE, meetingInfo.get(MeetingInfo.MEETING_TITLE));
1008         entityValues.put(Events.ORGANIZER, organizerEmail);
1009 
1010         // Add ourselves as an attendee, using our account email address
1011         ContentValues attendeeValues = new ContentValues();
1012         attendeeValues.put(Attendees.ATTENDEE_RELATIONSHIP,
1013                 Attendees.RELATIONSHIP_ATTENDEE);
1014         attendeeValues.put(Attendees.ATTENDEE_EMAIL, mAccount.mEmailAddress);
1015         entity.addSubValue(Attendees.CONTENT_URI, attendeeValues);
1016 
1017         // Add the organizer
1018         ContentValues organizerValues = new ContentValues();
1019         organizerValues.put(Attendees.ATTENDEE_RELATIONSHIP,
1020                 Attendees.RELATIONSHIP_ORGANIZER);
1021         organizerValues.put(Attendees.ATTENDEE_EMAIL, organizerEmail);
1022         entity.addSubValue(Attendees.CONTENT_URI, organizerValues);
1023 
1024         // Create a message from the Entity we've built.  The message will have fields like
1025         // to, subject, date, and text filled in.  There will also be an "inline" attachment
1026         // which is in iCalendar format
1027         int flag;
1028         switch(response) {
1029             case EmailServiceConstants.MEETING_REQUEST_ACCEPTED:
1030                 flag = Message.FLAG_OUTGOING_MEETING_ACCEPT;
1031                 break;
1032             case EmailServiceConstants.MEETING_REQUEST_DECLINED:
1033                 flag = Message.FLAG_OUTGOING_MEETING_DECLINE;
1034                 break;
1035             case EmailServiceConstants.MEETING_REQUEST_TENTATIVE:
1036             default:
1037                 flag = Message.FLAG_OUTGOING_MEETING_TENTATIVE;
1038                 break;
1039         }
1040         Message outgoingMsg =
1041             CalendarUtilities.createMessageForEntity(mContext, entity, flag,
1042                     meetingInfo.get(MeetingInfo.MEETING_UID), mAccount);
1043         // Assuming we got a message back (we might not if the event has been deleted), send it
1044         if (outgoingMsg != null) {
1045             EasOutboxService.sendMessage(mContext, mAccount.mId, outgoingMsg);
1046         }
1047     }
1048 
1049     /**
1050      * Responds to a meeting request.  The MeetingResponseRequest is basically our
1051      * wrapper for the meetingResponse service call
1052      * @param req the request (message id and response code)
1053      * @throws IOException
1054      */
sendMeetingResponse(MeetingResponseRequest req)1055     protected void sendMeetingResponse(MeetingResponseRequest req) throws IOException {
1056         // Retrieve the message and mailbox; punt if either are null
1057         Message msg = Message.restoreMessageWithId(mContext, req.mMessageId);
1058         if (msg == null) return;
1059         Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, msg.mMailboxKey);
1060         if (mailbox == null) return;
1061         Serializer s = new Serializer();
1062         s.start(Tags.MREQ_MEETING_RESPONSE).start(Tags.MREQ_REQUEST);
1063         s.data(Tags.MREQ_USER_RESPONSE, Integer.toString(req.mResponse));
1064         s.data(Tags.MREQ_COLLECTION_ID, mailbox.mServerId);
1065         s.data(Tags.MREQ_REQ_ID, msg.mServerId);
1066         s.end().end().done();
1067         HttpResponse res = sendHttpClientPost("MeetingResponse", s.toByteArray());
1068         int status = res.getStatusLine().getStatusCode();
1069         if (status == HttpStatus.SC_OK) {
1070             HttpEntity e = res.getEntity();
1071             int len = (int)e.getContentLength();
1072             InputStream is = res.getEntity().getContent();
1073             if (len != 0) {
1074                 new MeetingResponseParser(is, this).parse();
1075                 sendMeetingResponseMail(msg, req.mResponse);
1076             }
1077         } else if (isAuthError(status)) {
1078             throw new EasAuthenticationException();
1079         } else {
1080             userLog("Meeting response request failed, code: " + status);
1081             throw new IOException();
1082         }
1083     }
1084 
1085     /**
1086      * Using mUserName and mPassword, create and cache mAuthString and mCacheString, which are used
1087      * in all HttpPost commands.  This should be called if these strings are null, or if mUserName
1088      * and/or mPassword are changed
1089      */
1090     @SuppressWarnings("deprecation")
cacheAuthAndCmdString()1091     private void cacheAuthAndCmdString() {
1092         String safeUserName = URLEncoder.encode(mUserName);
1093         String cs = mUserName + ':' + mPassword;
1094         mAuthString = "Basic " + Base64.encodeToString(cs.getBytes(), Base64.NO_WRAP);
1095         mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId +
1096             "&DeviceType=" + mDeviceType;
1097     }
1098 
makeUriString(String cmd, String extra)1099     private String makeUriString(String cmd, String extra) throws IOException {
1100          // Cache the authentication string and the command string
1101         if (mAuthString == null || mCmdString == null) {
1102             cacheAuthAndCmdString();
1103         }
1104         String us = (mSsl ? (mTrustSsl ? "httpts" : "https") : "http") + "://" + mHostAddress +
1105             "/Microsoft-Server-ActiveSync";
1106         if (cmd != null) {
1107             us += "?Cmd=" + cmd + mCmdString;
1108         }
1109         if (extra != null) {
1110             us += extra;
1111         }
1112         return us;
1113     }
1114 
1115     /**
1116      * Set standard HTTP headers, using a policy key if required
1117      * @param method the method we are going to send
1118      * @param usePolicyKey whether or not a policy key should be sent in the headers
1119      */
setHeaders(HttpRequestBase method, boolean usePolicyKey)1120     /*package*/ void setHeaders(HttpRequestBase method, boolean usePolicyKey) {
1121         method.setHeader("Authorization", mAuthString);
1122         method.setHeader("MS-ASProtocolVersion", mProtocolVersion);
1123         method.setHeader("Connection", "keep-alive");
1124         method.setHeader("User-Agent", mDeviceType + '/' + Eas.VERSION);
1125         if (usePolicyKey) {
1126             // If there's an account in existence, use its key; otherwise (we're creating the
1127             // account), send "0".  The server will respond with code 449 if there are policies
1128             // to be enforced
1129             String key = "0";
1130             if (mAccount != null) {
1131                 String accountKey = mAccount.mSecuritySyncKey;
1132                 if (!TextUtils.isEmpty(accountKey)) {
1133                     key = accountKey;
1134                 }
1135             }
1136             method.setHeader("X-MS-PolicyKey", key);
1137         }
1138     }
1139 
getClientConnectionManager()1140     private ClientConnectionManager getClientConnectionManager() {
1141         return SyncManager.getClientConnectionManager();
1142     }
1143 
getHttpClient(int timeout)1144     private HttpClient getHttpClient(int timeout) {
1145         HttpParams params = new BasicHttpParams();
1146         HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT);
1147         HttpConnectionParams.setSoTimeout(params, timeout);
1148         HttpConnectionParams.setSocketBufferSize(params, 8192);
1149         HttpClient client = new DefaultHttpClient(getClientConnectionManager(), params);
1150         return client;
1151     }
1152 
sendHttpClientPost(String cmd, byte[] bytes)1153     protected HttpResponse sendHttpClientPost(String cmd, byte[] bytes) throws IOException {
1154         return sendHttpClientPost(cmd, new ByteArrayEntity(bytes), COMMAND_TIMEOUT);
1155     }
1156 
sendHttpClientPost(String cmd, HttpEntity entity)1157     protected HttpResponse sendHttpClientPost(String cmd, HttpEntity entity) throws IOException {
1158         return sendHttpClientPost(cmd, entity, COMMAND_TIMEOUT);
1159     }
1160 
sendPing(byte[] bytes, int heartbeat)1161     protected HttpResponse sendPing(byte[] bytes, int heartbeat) throws IOException {
1162        Thread.currentThread().setName(mAccount.mDisplayName + ": Ping");
1163        if (Eas.USER_LOG) {
1164            userLog("Send ping, timeout: " + heartbeat + "s, high: " + mPingHighWaterMark + 's');
1165        }
1166        return sendHttpClientPost(PING_COMMAND, new ByteArrayEntity(bytes), (heartbeat+5)*SECONDS);
1167     }
1168 
1169     /**
1170      * Convenience method for executePostWithTimeout for use other than with the Ping command
1171      */
executePostWithTimeout(HttpClient client, HttpPost method, int timeout)1172     protected HttpResponse executePostWithTimeout(HttpClient client, HttpPost method, int timeout)
1173             throws IOException {
1174         return executePostWithTimeout(client, method, timeout, false);
1175     }
1176 
1177     /**
1178      * Handle executing an HTTP POST command with proper timeout, watchdog, and ping behavior
1179      * @param client the HttpClient
1180      * @param method the HttpPost
1181      * @param timeout the timeout before failure, in ms
1182      * @param isPingCommand whether the POST is for the Ping command (requires wakelock logic)
1183      * @return the HttpResponse
1184      * @throws IOException
1185      */
executePostWithTimeout(HttpClient client, HttpPost method, int timeout, boolean isPingCommand)1186     protected HttpResponse executePostWithTimeout(HttpClient client, HttpPost method, int timeout,
1187             boolean isPingCommand) throws IOException {
1188         synchronized(getSynchronizer()) {
1189             mPendingPost = method;
1190             long alarmTime = timeout + WATCHDOG_TIMEOUT_ALLOWANCE;
1191             if (isPingCommand) {
1192                 SyncManager.runAsleep(mMailboxId, alarmTime);
1193             } else {
1194                 SyncManager.setWatchdogAlarm(mMailboxId, alarmTime);
1195             }
1196         }
1197         try {
1198             return client.execute(method);
1199         } finally {
1200             synchronized(getSynchronizer()) {
1201                 if (isPingCommand) {
1202                     SyncManager.runAwake(mMailboxId);
1203                 } else {
1204                     SyncManager.clearWatchdogAlarm(mMailboxId);
1205                 }
1206                 mPendingPost = null;
1207             }
1208         }
1209     }
1210 
sendHttpClientPost(String cmd, HttpEntity entity, int timeout)1211     protected HttpResponse sendHttpClientPost(String cmd, HttpEntity entity, int timeout)
1212             throws IOException {
1213         HttpClient client = getHttpClient(timeout);
1214         boolean isPingCommand = cmd.equals(PING_COMMAND);
1215 
1216         // Split the mail sending commands
1217         String extra = null;
1218         boolean msg = false;
1219         if (cmd.startsWith("SmartForward&") || cmd.startsWith("SmartReply&")) {
1220             int cmdLength = cmd.indexOf('&');
1221             extra = cmd.substring(cmdLength);
1222             cmd = cmd.substring(0, cmdLength);
1223             msg = true;
1224         } else if (cmd.startsWith("SendMail&")) {
1225             msg = true;
1226         }
1227 
1228         String us = makeUriString(cmd, extra);
1229         HttpPost method = new HttpPost(URI.create(us));
1230         // Send the proper Content-Type header
1231         // If entity is null (e.g. for attachments), don't set this header
1232         if (msg) {
1233             method.setHeader("Content-Type", "message/rfc822");
1234         } else if (entity != null) {
1235             method.setHeader("Content-Type", "application/vnd.ms-sync.wbxml");
1236         }
1237         setHeaders(method, !cmd.equals(PING_COMMAND));
1238         method.setEntity(entity);
1239         return executePostWithTimeout(client, method, timeout, isPingCommand);
1240     }
1241 
sendHttpClientOptions()1242     protected HttpResponse sendHttpClientOptions() throws IOException {
1243         HttpClient client = getHttpClient(COMMAND_TIMEOUT);
1244         String us = makeUriString("OPTIONS", null);
1245         HttpOptions method = new HttpOptions(URI.create(us));
1246         setHeaders(method, false);
1247         return client.execute(method);
1248     }
1249 
getTargetCollectionClassFromCursor(Cursor c)1250     String getTargetCollectionClassFromCursor(Cursor c) {
1251         int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
1252         if (type == Mailbox.TYPE_CONTACTS) {
1253             return "Contacts";
1254         } else if (type == Mailbox.TYPE_CALENDAR) {
1255             return "Calendar";
1256         } else {
1257             return "Email";
1258         }
1259     }
1260 
1261     /**
1262      * Negotiate provisioning with the server.  First, get policies form the server and see if
1263      * the policies are supported by the device.  Then, write the policies to the account and
1264      * tell SecurityPolicy that we have policies in effect.  Finally, see if those policies are
1265      * active; if so, acknowledge the policies to the server and get a final policy key that we
1266      * use in future EAS commands and write this key to the account.
1267      * @return whether or not provisioning has been successful
1268      * @throws IOException
1269      */
tryProvision()1270     private boolean tryProvision() throws IOException {
1271         // First, see if provisioning is even possible, i.e. do we support the policies required
1272         // by the server
1273         ProvisionParser pp = canProvision();
1274         if (pp != null) {
1275             SecurityPolicy sp = SecurityPolicy.getInstance(mContext);
1276             // Get the policies from ProvisionParser
1277             PolicySet ps = pp.getPolicySet();
1278             // Update the account with a null policyKey (the key we've gotten is
1279             // temporary and cannot be used for syncing)
1280             if (ps.writeAccount(mAccount, null, true, mContext)) {
1281                 sp.updatePolicies(mAccount.mId);
1282             }
1283             if (pp.getRemoteWipe()) {
1284                 // We've gotten a remote wipe command
1285                 SyncManager.alwaysLog("!!! Remote wipe request received");
1286                 // Start by setting the account to security hold
1287                 sp.setAccountHoldFlag(mAccount, true);
1288                 // Force a stop to any running syncs for this account (except this one)
1289                 SyncManager.stopNonAccountMailboxSyncsForAccount(mAccount.mId);
1290 
1291                 // If we're not the admin, we can't do the wipe, so just return
1292                 if (!sp.isActiveAdmin()) {
1293                     SyncManager.alwaysLog("!!! Not device admin; can't wipe");
1294                     return false;
1295                 }
1296                 // First, we've got to acknowledge it, but wrap the wipe in try/catch so that
1297                 // we wipe the device regardless of any errors in acknowledgment
1298                 try {
1299                     SyncManager.alwaysLog("!!! Acknowledging remote wipe to server");
1300                     acknowledgeRemoteWipe(pp.getPolicyKey());
1301                 } catch (Exception e) {
1302                     // Because remote wipe is such a high priority task, we don't want to
1303                     // circumvent it if there's an exception in acknowledgment
1304                 }
1305                 // Then, tell SecurityPolicy to wipe the device
1306                 SyncManager.alwaysLog("!!! Executing remote wipe");
1307                 sp.remoteWipe();
1308                 return false;
1309             } else if (sp.isActive(ps)) {
1310                 // See if the required policies are in force; if they are, acknowledge the policies
1311                 // to the server and get the final policy key
1312                 String policyKey = acknowledgeProvision(pp.getPolicyKey(), PROVISION_STATUS_OK);
1313                 if (policyKey != null) {
1314                     // Write the final policy key to the Account and say we've been successful
1315                     ps.writeAccount(mAccount, policyKey, true, mContext);
1316                     // Release any mailboxes that might be in a security hold
1317                     SyncManager.releaseSecurityHold(mAccount);
1318                     return true;
1319                 }
1320             } else {
1321                 // Notify that we are blocked because of policies
1322                 sp.policiesRequired(mAccount.mId);
1323             }
1324         }
1325         return false;
1326     }
1327 
getPolicyType()1328     private String getPolicyType() {
1329         return (mProtocolVersionDouble >=
1330             Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) ? EAS_12_POLICY_TYPE : EAS_2_POLICY_TYPE;
1331     }
1332 
1333     /**
1334      * Obtain a set of policies from the server and determine whether those policies are supported
1335      * by the device.
1336      * @return the ProvisionParser (holds policies and key) if we receive policies and they are
1337      * supported by the device; null otherwise
1338      * @throws IOException
1339      */
canProvision()1340     private ProvisionParser canProvision() throws IOException {
1341         Serializer s = new Serializer();
1342         s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES);
1343         s.start(Tags.PROVISION_POLICY).data(Tags.PROVISION_POLICY_TYPE, getPolicyType())
1344             .end().end().end().done();
1345         HttpResponse resp = sendHttpClientPost("Provision", s.toByteArray());
1346         int code = resp.getStatusLine().getStatusCode();
1347         if (code == HttpStatus.SC_OK) {
1348             InputStream is = resp.getEntity().getContent();
1349             ProvisionParser pp = new ProvisionParser(is, this);
1350             if (pp.parse()) {
1351                 // The PolicySet in the ProvisionParser will have the requirements for all KNOWN
1352                 // policies.  If others are required, hasSupportablePolicySet will be false
1353                 if (pp.hasSupportablePolicySet()) {
1354                     // If the policies are supportable (in this context, meaning that there are no
1355                     // completely unimplemented policies required), just return the parser itself
1356                     return pp;
1357                 } else {
1358                     // Try to acknowledge using the "partial" status (i.e. we can partially
1359                     // accommodate the required policies).  The server will agree to this if the
1360                     // "allow non-provisionable devices" setting is enabled on the server
1361                     String policyKey = acknowledgeProvision(pp.getPolicyKey(),
1362                             PROVISION_STATUS_PARTIAL);
1363                     // Return either the parser (success) or null (failure)
1364                     return (policyKey != null) ? pp : null;
1365                 }
1366             }
1367         }
1368         // On failures, simply return null
1369         return null;
1370     }
1371 
1372     /**
1373      * Acknowledge that we support the policies provided by the server, and that these policies
1374      * are in force.
1375      * @param tempKey the initial (temporary) policy key sent by the server
1376      * @return the final policy key, which can be used for syncing
1377      * @throws IOException
1378      */
acknowledgeRemoteWipe(String tempKey)1379     private void acknowledgeRemoteWipe(String tempKey) throws IOException {
1380         acknowledgeProvisionImpl(tempKey, PROVISION_STATUS_OK, true);
1381     }
1382 
acknowledgeProvision(String tempKey, String result)1383     private String acknowledgeProvision(String tempKey, String result) throws IOException {
1384         return acknowledgeProvisionImpl(tempKey, result, false);
1385     }
1386 
acknowledgeProvisionImpl(String tempKey, String status, boolean remoteWipe)1387     private String acknowledgeProvisionImpl(String tempKey, String status,
1388             boolean remoteWipe) throws IOException {
1389         Serializer s = new Serializer();
1390         s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES);
1391         s.start(Tags.PROVISION_POLICY);
1392 
1393         // Use the proper policy type, depending on EAS version
1394         s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType());
1395 
1396         s.data(Tags.PROVISION_POLICY_KEY, tempKey);
1397         s.data(Tags.PROVISION_STATUS, status);
1398         s.end().end(); // PROVISION_POLICY, PROVISION_POLICIES
1399         if (remoteWipe) {
1400             s.start(Tags.PROVISION_REMOTE_WIPE);
1401             s.data(Tags.PROVISION_STATUS, PROVISION_STATUS_OK);
1402             s.end();
1403         }
1404         s.end().done(); // PROVISION_PROVISION
1405         HttpResponse resp = sendHttpClientPost("Provision", s.toByteArray());
1406         int code = resp.getStatusLine().getStatusCode();
1407         if (code == HttpStatus.SC_OK) {
1408             InputStream is = resp.getEntity().getContent();
1409             ProvisionParser pp = new ProvisionParser(is, this);
1410             if (pp.parse()) {
1411                 // Return the final policy key from the ProvisionParser
1412                 return pp.getPolicyKey();
1413             }
1414         }
1415         // On failures, return null
1416         return null;
1417     }
1418 
1419     /**
1420      * Performs FolderSync
1421      *
1422      * @throws IOException
1423      * @throws EasParserException
1424      */
runAccountMailbox()1425     public void runAccountMailbox() throws IOException, EasParserException {
1426         // Initialize exit status to success
1427         mExitStatus = EmailServiceStatus.SUCCESS;
1428         try {
1429             try {
1430                 SyncManager.callback()
1431                     .syncMailboxListStatus(mAccount.mId, EmailServiceStatus.IN_PROGRESS, 0);
1432             } catch (RemoteException e1) {
1433                 // Don't care if this fails
1434             }
1435 
1436             if (mAccount.mSyncKey == null) {
1437                 mAccount.mSyncKey = "0";
1438                 userLog("Account syncKey INIT to 0");
1439                 ContentValues cv = new ContentValues();
1440                 cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
1441                 mAccount.update(mContext, cv);
1442             }
1443 
1444             boolean firstSync = mAccount.mSyncKey.equals("0");
1445             if (firstSync) {
1446                 userLog("Initial FolderSync");
1447             }
1448 
1449             // When we first start up, change all mailboxes to push.
1450             ContentValues cv = new ContentValues();
1451             cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH);
1452             if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
1453                     WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING,
1454                     new String[] {Long.toString(mAccount.mId)}) > 0) {
1455                 SyncManager.kick("change ping boxes to push");
1456             }
1457 
1458             // Determine our protocol version, if we haven't already and save it in the Account
1459             // Also re-check protocol version at least once a day (in case of upgrade)
1460             if (mAccount.mProtocolVersion == null ||
1461                     ((System.currentTimeMillis() - mMailbox.mSyncTime) > DAYS)) {
1462                 userLog("Determine EAS protocol version");
1463                 HttpResponse resp = sendHttpClientOptions();
1464                 int code = resp.getStatusLine().getStatusCode();
1465                 userLog("OPTIONS response: ", code);
1466                 if (code == HttpStatus.SC_OK) {
1467                     Header header = resp.getFirstHeader("MS-ASProtocolCommands");
1468                     userLog(header.getValue());
1469                     header = resp.getFirstHeader("ms-asprotocolversions");
1470                     try {
1471                         setupProtocolVersion(this, header);
1472                     } catch (MessagingException e) {
1473                         // Since we've already validated, this can't really happen
1474                         // But if it does, we'll rethrow this...
1475                         throw new IOException();
1476                     }
1477                     // Save the protocol version
1478                     cv.clear();
1479                     // Save the protocol version in the account
1480                     cv.put(Account.PROTOCOL_VERSION, mProtocolVersion);
1481                     mAccount.update(mContext, cv);
1482                     cv.clear();
1483                     // Save the sync time of the account mailbox to current time
1484                     cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
1485                     mMailbox.update(mContext, cv);
1486                  } else {
1487                     errorLog("OPTIONS command failed; throwing IOException");
1488                     throw new IOException();
1489                 }
1490             }
1491 
1492             // Change all pushable boxes to push when we start the account mailbox
1493             if (mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH) {
1494                 cv.clear();
1495                 cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH);
1496                 if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
1497                         SyncManager.WHERE_IN_ACCOUNT_AND_PUSHABLE,
1498                         new String[] {Long.toString(mAccount.mId)}) > 0) {
1499                     userLog("Push account; set pushable boxes to push...");
1500                 }
1501             }
1502 
1503             while (!mStop) {
1504                 userLog("Sending Account syncKey: ", mAccount.mSyncKey);
1505                 Serializer s = new Serializer();
1506                 s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY)
1507                     .text(mAccount.mSyncKey).end().end().done();
1508                 HttpResponse resp = sendHttpClientPost("FolderSync", s.toByteArray());
1509                 if (mStop) break;
1510                 int code = resp.getStatusLine().getStatusCode();
1511                 if (code == HttpStatus.SC_OK) {
1512                     HttpEntity entity = resp.getEntity();
1513                     int len = (int)entity.getContentLength();
1514                     if (len != 0) {
1515                         InputStream is = entity.getContent();
1516                         // Returns true if we need to sync again
1517                         if (new FolderSyncParser(is, new AccountSyncAdapter(mMailbox, this))
1518                                 .parse()) {
1519                             continue;
1520                         }
1521                     }
1522                 } else if (isProvisionError(code)) {
1523                     // If the sync error is a provisioning failure (perhaps the policies changed),
1524                     // let's try the provisioning procedure
1525                     // Provisioning must only be attempted for the account mailbox - trying to
1526                     // provision any other mailbox may result in race conditions and the creation
1527                     // of multiple policy keys.
1528                     if (!tryProvision()) {
1529                         // Set the appropriate failure status
1530                         mExitStatus = EXIT_SECURITY_FAILURE;
1531                         return;
1532                     } else {
1533                         // If we succeeded, try again...
1534                         continue;
1535                     }
1536                 } else if (isAuthError(code)) {
1537                     mExitStatus = EXIT_LOGIN_FAILURE;
1538                     return;
1539                 } else {
1540                     userLog("FolderSync response error: ", code);
1541                 }
1542 
1543                 // Change all push/hold boxes to push
1544                 cv.clear();
1545                 cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH);
1546                 if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
1547                         WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX,
1548                         new String[] {Long.toString(mAccount.mId)}) > 0) {
1549                     userLog("Set push/hold boxes to push...");
1550                 }
1551 
1552                 try {
1553                     SyncManager.callback()
1554                         .syncMailboxListStatus(mAccount.mId, mExitStatus, 0);
1555                 } catch (RemoteException e1) {
1556                     // Don't care if this fails
1557                 }
1558 
1559                 // Before each run of the pingLoop, if this Account has a PolicySet, make sure it's
1560                 // active; otherwise, clear out the key/flag.  This should cause a provisioning
1561                 // error on the next POST, and start the security sequence over again
1562                 String key = mAccount.mSecuritySyncKey;
1563                 if (!TextUtils.isEmpty(key)) {
1564                     PolicySet ps = new PolicySet(mAccount);
1565                     SecurityPolicy sp = SecurityPolicy.getInstance(mContext);
1566                     if (!sp.isActive(ps)) {
1567                         cv.clear();
1568                         cv.put(AccountColumns.SECURITY_FLAGS, 0);
1569                         cv.putNull(AccountColumns.SECURITY_SYNC_KEY);
1570                         long accountId = mAccount.mId;
1571                         mContentResolver.update(ContentUris.withAppendedId(
1572                                 Account.CONTENT_URI, accountId), cv, null, null);
1573                         sp.policiesRequired(accountId);
1574                     }
1575                 }
1576 
1577                 // Wait for push notifications.
1578                 String threadName = Thread.currentThread().getName();
1579                 try {
1580                     runPingLoop();
1581                 } catch (StaleFolderListException e) {
1582                     // We break out if we get told about a stale folder list
1583                     userLog("Ping interrupted; folder list requires sync...");
1584                 } catch (IllegalHeartbeatException e) {
1585                     // If we're sending an illegal heartbeat, reset either the min or the max to
1586                     // that heartbeat
1587                     resetHeartbeats(e.mLegalHeartbeat);
1588                 } finally {
1589                     Thread.currentThread().setName(threadName);
1590                 }
1591             }
1592          } catch (IOException e) {
1593             // We catch this here to send the folder sync status callback
1594             // A folder sync failed callback will get sent from run()
1595             try {
1596                 if (!mStop) {
1597                     SyncManager.callback()
1598                         .syncMailboxListStatus(mAccount.mId,
1599                                 EmailServiceStatus.CONNECTION_ERROR, 0);
1600                 }
1601             } catch (RemoteException e1) {
1602                 // Don't care if this fails
1603             }
1604             throw e;
1605         }
1606     }
1607 
1608     /**
1609      * Reset either our minimum or maximum ping heartbeat to a heartbeat known to be legal
1610      * @param legalHeartbeat a known legal heartbeat (from the EAS server)
1611      */
resetHeartbeats(int legalHeartbeat)1612     /*package*/ void resetHeartbeats(int legalHeartbeat) {
1613         userLog("Resetting min/max heartbeat, legal = " + legalHeartbeat);
1614         // We are here because the current heartbeat (mPingHeartbeat) is invalid.  Depending on
1615         // whether the argument is above or below the current heartbeat, we can infer the need to
1616         // change either the minimum or maximum heartbeat
1617         if (legalHeartbeat > mPingHeartbeat) {
1618             // The legal heartbeat is higher than the ping heartbeat; therefore, our minimum was
1619             // too low.  We respond by raising either or both of the minimum heartbeat or the
1620             // force heartbeat to the argument value
1621             if (mPingMinHeartbeat < legalHeartbeat) {
1622                 mPingMinHeartbeat = legalHeartbeat;
1623             }
1624             if (mPingForceHeartbeat < legalHeartbeat) {
1625                 mPingForceHeartbeat = legalHeartbeat;
1626             }
1627             // If our minimum is now greater than the max, bring them together
1628             if (mPingMinHeartbeat > mPingMaxHeartbeat) {
1629                 mPingMaxHeartbeat = legalHeartbeat;
1630             }
1631         } else if (legalHeartbeat < mPingHeartbeat) {
1632             // The legal heartbeat is lower than the ping heartbeat; therefore, our maximum was
1633             // too high.  We respond by lowering the maximum to the argument value
1634             mPingMaxHeartbeat = legalHeartbeat;
1635             // If our maximum is now less than the minimum, bring them together
1636             if (mPingMaxHeartbeat < mPingMinHeartbeat) {
1637                 mPingMinHeartbeat = legalHeartbeat;
1638             }
1639         }
1640         // Set current heartbeat to the legal heartbeat
1641         mPingHeartbeat = legalHeartbeat;
1642         // Allow the heartbeat logic to run
1643         mPingHeartbeatDropped = false;
1644     }
1645 
pushFallback(long mailboxId)1646     private void pushFallback(long mailboxId) {
1647         Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
1648         if (mailbox == null) {
1649             return;
1650         }
1651         ContentValues cv = new ContentValues();
1652         int mins = PING_FALLBACK_PIM;
1653         if (mailbox.mType == Mailbox.TYPE_INBOX) {
1654             mins = PING_FALLBACK_INBOX;
1655         }
1656         cv.put(Mailbox.SYNC_INTERVAL, mins);
1657         mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
1658                 cv, null, null);
1659         errorLog("*** PING ERROR LOOP: Set " + mailbox.mDisplayName + " to " + mins + " min sync");
1660         SyncManager.kick("push fallback");
1661     }
1662 
1663     /**
1664      * Simplistic attempt to determine a NAT timeout, based on experience with various carriers
1665      * and networks.  The string "reset by peer" is very common in these situations, so we look for
1666      * that specifically.  We may add additional tests here as more is learned.
1667      * @param message
1668      * @return whether this message is likely associated with a NAT failure
1669      */
isLikelyNatFailure(String message)1670     private boolean isLikelyNatFailure(String message) {
1671         if (message == null) return false;
1672         if (message.contains("reset by peer")) {
1673             return true;
1674         }
1675         return false;
1676     }
1677 
runPingLoop()1678     private void runPingLoop() throws IOException, StaleFolderListException,
1679             IllegalHeartbeatException {
1680         int pingHeartbeat = mPingHeartbeat;
1681         userLog("runPingLoop");
1682         // Do push for all sync services here
1683         long endTime = System.currentTimeMillis() + (30*MINUTES);
1684         HashMap<String, Integer> pingErrorMap = new HashMap<String, Integer>();
1685         ArrayList<String> readyMailboxes = new ArrayList<String>();
1686         ArrayList<String> notReadyMailboxes = new ArrayList<String>();
1687         int pingWaitCount = 0;
1688 
1689         while ((System.currentTimeMillis() < endTime) && !mStop) {
1690             // Count of pushable mailboxes
1691             int pushCount = 0;
1692             // Count of mailboxes that can be pushed right now
1693             int canPushCount = 0;
1694             // Count of uninitialized boxes
1695             int uninitCount = 0;
1696 
1697             Serializer s = new Serializer();
1698             Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
1699                     MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId +
1700                     AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null, null);
1701             notReadyMailboxes.clear();
1702             readyMailboxes.clear();
1703             try {
1704                 // Loop through our pushed boxes seeing what is available to push
1705                 while (c.moveToNext()) {
1706                     pushCount++;
1707                     // Two requirements for push:
1708                     // 1) SyncManager tells us the mailbox is syncable (not running, not stopped)
1709                     // 2) The syncKey isn't "0" (i.e. it's synced at least once)
1710                     long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN);
1711                     int pingStatus = SyncManager.pingStatus(mailboxId);
1712                     String mailboxName = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
1713                     if (pingStatus == SyncManager.PING_STATUS_OK) {
1714                         String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN);
1715                         if ((syncKey == null) || syncKey.equals("0")) {
1716                             // We can't push until the initial sync is done
1717                             pushCount--;
1718                             uninitCount++;
1719                             continue;
1720                         }
1721 
1722                         if (canPushCount++ == 0) {
1723                             // Initialize the Ping command
1724                             s.start(Tags.PING_PING)
1725                                 .data(Tags.PING_HEARTBEAT_INTERVAL,
1726                                         Integer.toString(pingHeartbeat))
1727                                 .start(Tags.PING_FOLDERS);
1728                         }
1729 
1730                         String folderClass = getTargetCollectionClassFromCursor(c);
1731                         s.start(Tags.PING_FOLDER)
1732                             .data(Tags.PING_ID, c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN))
1733                             .data(Tags.PING_CLASS, folderClass)
1734                             .end();
1735                         readyMailboxes.add(mailboxName);
1736                     } else if ((pingStatus == SyncManager.PING_STATUS_RUNNING) ||
1737                             (pingStatus == SyncManager.PING_STATUS_WAITING)) {
1738                         notReadyMailboxes.add(mailboxName);
1739                     } else if (pingStatus == SyncManager.PING_STATUS_UNABLE) {
1740                         pushCount--;
1741                         userLog(mailboxName, " in error state; ignore");
1742                         continue;
1743                     }
1744                 }
1745             } finally {
1746                 c.close();
1747             }
1748 
1749             if (Eas.USER_LOG) {
1750                 if (!notReadyMailboxes.isEmpty()) {
1751                     userLog("Ping not ready for: " + notReadyMailboxes);
1752                 }
1753                 if (!readyMailboxes.isEmpty()) {
1754                     userLog("Ping ready for: " + readyMailboxes);
1755                 }
1756             }
1757 
1758             // If we've waited 10 seconds or more, just ping with whatever boxes are ready
1759             // But use a shorter than normal heartbeat
1760             boolean forcePing = !notReadyMailboxes.isEmpty() && (pingWaitCount > 5);
1761 
1762             if ((canPushCount > 0) && ((canPushCount == pushCount) || forcePing)) {
1763                 // If all pingable boxes are ready for push, send Ping to the server
1764                 s.end().end().done();
1765                 pingWaitCount = 0;
1766                 mPostReset = false;
1767                 mPostAborted = false;
1768 
1769                 // If we've been stopped, this is a good time to return
1770                 if (mStop) return;
1771 
1772                 long pingTime = SystemClock.elapsedRealtime();
1773                 try {
1774                     // Send the ping, wrapped by appropriate timeout/alarm
1775                     if (forcePing) {
1776                         userLog("Forcing ping after waiting for all boxes to be ready");
1777                     }
1778                     HttpResponse res =
1779                         sendPing(s.toByteArray(), forcePing ? mPingForceHeartbeat : pingHeartbeat);
1780 
1781                     int code = res.getStatusLine().getStatusCode();
1782                     userLog("Ping response: ", code);
1783 
1784                     // Return immediately if we've been asked to stop during the ping
1785                     if (mStop) {
1786                         userLog("Stopping pingLoop");
1787                         return;
1788                     }
1789 
1790                     if (code == HttpStatus.SC_OK) {
1791                         // Make sure to clear out any pending sync errors
1792                         SyncManager.removeFromSyncErrorMap(mMailboxId);
1793                         HttpEntity e = res.getEntity();
1794                         int len = (int)e.getContentLength();
1795                         InputStream is = res.getEntity().getContent();
1796                         if (len != 0) {
1797                             int pingResult = parsePingResult(is, mContentResolver, pingErrorMap);
1798                             // If our ping completed (status = 1), and we weren't forced and we're
1799                             // not at the maximum, try increasing timeout by two minutes
1800                             if (pingResult == PROTOCOL_PING_STATUS_COMPLETED && !forcePing) {
1801                                 if (pingHeartbeat > mPingHighWaterMark) {
1802                                     mPingHighWaterMark = pingHeartbeat;
1803                                     userLog("Setting high water mark at: ", mPingHighWaterMark);
1804                                 }
1805                                 if ((pingHeartbeat < mPingMaxHeartbeat) &&
1806                                         !mPingHeartbeatDropped) {
1807                                     pingHeartbeat += PING_HEARTBEAT_INCREMENT;
1808                                     if (pingHeartbeat > mPingMaxHeartbeat) {
1809                                         pingHeartbeat = mPingMaxHeartbeat;
1810                                     }
1811                                     userLog("Increasing ping heartbeat to ", pingHeartbeat, "s");
1812                                 }
1813                             }
1814                         } else {
1815                             userLog("Ping returned empty result; throwing IOException");
1816                             throw new IOException();
1817                         }
1818                     } else if (isAuthError(code)) {
1819                         mExitStatus = EXIT_LOGIN_FAILURE;
1820                         userLog("Authorization error during Ping: ", code);
1821                         throw new IOException();
1822                     }
1823                 } catch (IOException e) {
1824                     String message = e.getMessage();
1825                     // If we get the exception that is indicative of a NAT timeout and if we
1826                     // haven't yet "fixed" the timeout, back off by two minutes and "fix" it
1827                     boolean hasMessage = message != null;
1828                     userLog("IOException runPingLoop: " + (hasMessage ? message : "[no message]"));
1829                     if (mPostReset) {
1830                         // Nothing to do in this case; this is SyncManager telling us to try another
1831                         // ping.
1832                     } else if (mPostAborted || isLikelyNatFailure(message)) {
1833                         long pingLength = SystemClock.elapsedRealtime() - pingTime;
1834                         if ((pingHeartbeat > mPingMinHeartbeat) &&
1835                                 (pingHeartbeat > mPingHighWaterMark)) {
1836                             pingHeartbeat -= PING_HEARTBEAT_INCREMENT;
1837                             mPingHeartbeatDropped = true;
1838                             if (pingHeartbeat < mPingMinHeartbeat) {
1839                                 pingHeartbeat = mPingMinHeartbeat;
1840                             }
1841                             userLog("Decreased ping heartbeat to ", pingHeartbeat, "s");
1842                         } else if (mPostAborted) {
1843                             // There's no point in throwing here; this can happen in two cases
1844                             // 1) An alarm, which indicates minutes without activity; no sense
1845                             //    backing off
1846                             // 2) SyncManager abort, due to sync of mailbox.  Again, we want to
1847                             //    keep on trying to ping
1848                             userLog("Ping aborted; retry");
1849                         } else if (pingLength < 2000) {
1850                             userLog("Abort or NAT type return < 2 seconds; throwing IOException");
1851                             throw e;
1852                         } else {
1853                             userLog("NAT type IOException");
1854                         }
1855                     } else if (hasMessage && message.contains("roken pipe")) {
1856                         // The "broken pipe" error (uppercase or lowercase "b") seems to be an
1857                         // internal error, so let's not throw an exception (which leads to delays)
1858                         // but rather simply run through the loop again
1859                     } else {
1860                         throw e;
1861                     }
1862                 }
1863             } else if (forcePing) {
1864                 // In this case, there aren't any boxes that are pingable, but there are boxes
1865                 // waiting (for IOExceptions)
1866                 userLog("pingLoop waiting 60s for any pingable boxes");
1867                 sleep(60*SECONDS, true);
1868             } else if (pushCount > 0) {
1869                 // If we want to Ping, but can't just yet, wait a little bit
1870                 // TODO Change sleep to wait and use notify from SyncManager when a sync ends
1871                 sleep(2*SECONDS, false);
1872                 pingWaitCount++;
1873                 //userLog("pingLoop waited 2s for: ", (pushCount - canPushCount), " box(es)");
1874             } else if (uninitCount > 0) {
1875                 // In this case, we're doing an initial sync of at least one mailbox.  Since this
1876                 // is typically a one-time case, I'm ok with trying again every 10 seconds until
1877                 // we're in one of the other possible states.
1878                 userLog("pingLoop waiting for initial sync of ", uninitCount, " box(es)");
1879                 sleep(10*SECONDS, true);
1880             } else {
1881                 // We've got nothing to do, so we'll check again in 20 minutes at which time
1882                 // we'll update the folder list, check for policy changes and/or remote wipe, etc.
1883                 // Let the device sleep in the meantime...
1884                 userLog(ACCOUNT_MAILBOX_SLEEP_TEXT);
1885                 sleep(ACCOUNT_MAILBOX_SLEEP_TIME, true);
1886             }
1887         }
1888 
1889         // Save away the current heartbeat
1890         mPingHeartbeat = pingHeartbeat;
1891     }
1892 
sleep(long ms, boolean runAsleep)1893     private void sleep(long ms, boolean runAsleep) {
1894         if (runAsleep) {
1895             SyncManager.runAsleep(mMailboxId, ms+(5*SECONDS));
1896         }
1897         try {
1898             Thread.sleep(ms);
1899         } catch (InterruptedException e) {
1900             // Doesn't matter whether we stop early; it's the thought that counts
1901         } finally {
1902             if (runAsleep) {
1903                 SyncManager.runAwake(mMailboxId);
1904             }
1905         }
1906     }
1907 
parsePingResult(InputStream is, ContentResolver cr, HashMap<String, Integer> errorMap)1908     private int parsePingResult(InputStream is, ContentResolver cr,
1909             HashMap<String, Integer> errorMap)
1910             throws IOException, StaleFolderListException, IllegalHeartbeatException {
1911         PingParser pp = new PingParser(is, this);
1912         if (pp.parse()) {
1913             // True indicates some mailboxes need syncing...
1914             // syncList has the serverId's of the mailboxes...
1915             mBindArguments[0] = Long.toString(mAccount.mId);
1916             mPingChangeList = pp.getSyncList();
1917             for (String serverId: mPingChangeList) {
1918                 mBindArguments[1] = serverId;
1919                 Cursor c = cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
1920                         WHERE_ACCOUNT_KEY_AND_SERVER_ID, mBindArguments, null);
1921                 try {
1922                     if (c.moveToFirst()) {
1923 
1924                         /**
1925                          * Check the boxes reporting changes to see if there really were any...
1926                          * We do this because bugs in various Exchange servers can put us into a
1927                          * looping behavior by continually reporting changes in a mailbox, even when
1928                          * there aren't any.
1929                          *
1930                          * This behavior is seemingly random, and therefore we must code defensively
1931                          * by backing off of push behavior when it is detected.
1932                          *
1933                          * One known cause, on certain Exchange 2003 servers, is acknowledged by
1934                          * Microsoft, and the server hotfix for this case can be found at
1935                          * http://support.microsoft.com/kb/923282
1936                          */
1937 
1938                         // Check the status of the last sync
1939                         String status = c.getString(Mailbox.CONTENT_SYNC_STATUS_COLUMN);
1940                         int type = SyncManager.getStatusType(status);
1941                         // This check should always be true...
1942                         if (type == SyncManager.SYNC_PING) {
1943                             int changeCount = SyncManager.getStatusChangeCount(status);
1944                             if (changeCount > 0) {
1945                                 errorMap.remove(serverId);
1946                             } else if (changeCount == 0) {
1947                                 // This means that a ping reported changes in error; we keep a count
1948                                 // of consecutive errors of this kind
1949                                 String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
1950                                 Integer failures = errorMap.get(serverId);
1951                                 if (failures == null) {
1952                                     userLog("Last ping reported changes in error for: ", name);
1953                                     errorMap.put(serverId, 1);
1954                                 } else if (failures > MAX_PING_FAILURES) {
1955                                     // We'll back off of push for this box
1956                                     pushFallback(c.getLong(Mailbox.CONTENT_ID_COLUMN));
1957                                     continue;
1958                                 } else {
1959                                     userLog("Last ping reported changes in error for: ", name);
1960                                     errorMap.put(serverId, failures + 1);
1961                                 }
1962                             }
1963                         }
1964 
1965                         // If there were no problems with previous sync, we'll start another one
1966                         SyncManager.startManualSync(c.getLong(Mailbox.CONTENT_ID_COLUMN),
1967                                 SyncManager.SYNC_PING, null);
1968                     }
1969                 } finally {
1970                     c.close();
1971                 }
1972             }
1973         }
1974         return pp.getSyncStatus();
1975     }
1976 
getEmailFilter()1977     private String getEmailFilter() {
1978         String filter = Eas.FILTER_1_WEEK;
1979         switch (mAccount.mSyncLookback) {
1980             case com.android.email.Account.SYNC_WINDOW_1_DAY: {
1981                 filter = Eas.FILTER_1_DAY;
1982                 break;
1983             }
1984             case com.android.email.Account.SYNC_WINDOW_3_DAYS: {
1985                 filter = Eas.FILTER_3_DAYS;
1986                 break;
1987             }
1988             case com.android.email.Account.SYNC_WINDOW_1_WEEK: {
1989                 filter = Eas.FILTER_1_WEEK;
1990                 break;
1991             }
1992             case com.android.email.Account.SYNC_WINDOW_2_WEEKS: {
1993                 filter = Eas.FILTER_2_WEEKS;
1994                 break;
1995             }
1996             case com.android.email.Account.SYNC_WINDOW_1_MONTH: {
1997                 filter = Eas.FILTER_1_MONTH;
1998                 break;
1999             }
2000             case com.android.email.Account.SYNC_WINDOW_ALL: {
2001                 filter = Eas.FILTER_ALL;
2002                 break;
2003             }
2004         }
2005         return filter;
2006     }
2007 
2008     /**
2009      * Common code to sync E+PIM data
2010      *
2011      * @param target, an EasMailbox, EasContacts, or EasCalendar object
2012      */
sync(AbstractSyncAdapter target)2013     public void sync(AbstractSyncAdapter target) throws IOException {
2014         Mailbox mailbox = target.mMailbox;
2015 
2016         boolean moreAvailable = true;
2017         int loopingCount = 0;
2018         while (!mStop && moreAvailable) {
2019             // If we have no connectivity, just exit cleanly.  SyncManager will start us up again
2020             // when connectivity has returned
2021             if (!hasConnectivity()) {
2022                 userLog("No connectivity in sync; finishing sync");
2023                 mExitStatus = EXIT_DONE;
2024                 return;
2025             }
2026 
2027             // Every time through the loop we check to see if we're still syncable
2028             if (!target.isSyncable()) {
2029                 mExitStatus = EXIT_DONE;
2030                 return;
2031             }
2032 
2033             // Now, handle various requests
2034             while (true) {
2035                 Request req = null;
2036                 synchronized (mRequests) {
2037                     if (mRequests.isEmpty()) {
2038                         break;
2039                     } else {
2040                         req = mRequests.get(0);
2041                     }
2042                 }
2043 
2044                 // Our two request types are PartRequest (loading attachment) and
2045                 // MeetingResponseRequest (respond to a meeting request)
2046                 if (req instanceof PartRequest) {
2047                     getAttachment((PartRequest)req);
2048                 } else if (req instanceof MeetingResponseRequest) {
2049                     sendMeetingResponse((MeetingResponseRequest)req);
2050                 }
2051 
2052                 // If there's an exception handling the request, we'll throw it
2053                 // Otherwise, we remove the request
2054                 synchronized(mRequests) {
2055                     mRequests.remove(req);
2056                 }
2057             }
2058 
2059             Serializer s = new Serializer();
2060 
2061             String className = target.getCollectionName();
2062             String syncKey = target.getSyncKey();
2063             userLog("sync, sending ", className, " syncKey: ", syncKey);
2064             s.start(Tags.SYNC_SYNC)
2065                 .start(Tags.SYNC_COLLECTIONS)
2066                 .start(Tags.SYNC_COLLECTION)
2067                 .data(Tags.SYNC_CLASS, className)
2068                 .data(Tags.SYNC_SYNC_KEY, syncKey)
2069                 .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId);
2070 
2071             // Start with the default timeout
2072             int timeout = COMMAND_TIMEOUT;
2073             if (!syncKey.equals("0")) {
2074                 // EAS doesn't allow GetChanges in an initial sync; sending other options
2075                 // appears to cause the server to delay its response in some cases, and this delay
2076                 // can be long enough to result in an IOException and total failure to sync.
2077                 // Therefore, we don't send any options with the initial sync.
2078                 s.tag(Tags.SYNC_DELETES_AS_MOVES);
2079                 s.tag(Tags.SYNC_GET_CHANGES);
2080                 s.data(Tags.SYNC_WINDOW_SIZE,
2081                         className.equals("Email") ? EMAIL_WINDOW_SIZE : PIM_WINDOW_SIZE);
2082                 // Handle options
2083                 s.start(Tags.SYNC_OPTIONS);
2084                 // Set the lookback appropriately (EAS calls this a "filter") for all but Contacts
2085                 if (className.equals("Email")) {
2086                     s.data(Tags.SYNC_FILTER_TYPE, getEmailFilter());
2087                 } else if (className.equals("Calendar")) {
2088                     // TODO Force two weeks for calendar until we can set this!
2089                     s.data(Tags.SYNC_FILTER_TYPE, Eas.FILTER_2_WEEKS);
2090                 }
2091                 // Set the truncation amount for all classes
2092                 if (mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
2093                     s.start(Tags.BASE_BODY_PREFERENCE)
2094                     // HTML for email; plain text for everything else
2095                     .data(Tags.BASE_TYPE, (className.equals("Email") ? Eas.BODY_PREFERENCE_HTML
2096                             : Eas.BODY_PREFERENCE_TEXT))
2097                             .data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE)
2098                             .end();
2099                 } else {
2100                     s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
2101                 }
2102                 s.end();
2103             } else {
2104                 // Use enormous timeout for initial sync, which empirically can take a while longer
2105                 timeout = 120*SECONDS;
2106             }
2107             // Send our changes up to the server
2108             target.sendLocalChanges(s);
2109 
2110             s.end().end().end().done();
2111             HttpResponse resp = sendHttpClientPost("Sync", new ByteArrayEntity(s.toByteArray()),
2112                     timeout);
2113             int code = resp.getStatusLine().getStatusCode();
2114             if (code == HttpStatus.SC_OK) {
2115                 InputStream is = resp.getEntity().getContent();
2116                 if (is != null) {
2117                     moreAvailable = target.parse(is);
2118                     if (target.isLooping()) {
2119                         loopingCount++;
2120                         userLog("** Looping: " + loopingCount);
2121                         // After the maximum number of loops, we'll set moreAvailable to false and
2122                         // allow the sync loop to terminate
2123                         if (moreAvailable && (loopingCount > MAX_LOOPING_COUNT)) {
2124                             userLog("** Looping force stopped");
2125                             moreAvailable = false;
2126                         }
2127                     } else {
2128                         loopingCount = 0;
2129                     }
2130                     target.cleanup();
2131                 } else {
2132                     userLog("Empty input stream in sync command response");
2133                 }
2134             } else {
2135                 userLog("Sync response error: ", code);
2136                 if (isProvisionError(code)) {
2137                     mExitStatus = EXIT_SECURITY_FAILURE;
2138                 } else if (isAuthError(code)) {
2139                     mExitStatus = EXIT_LOGIN_FAILURE;
2140                 } else {
2141                     mExitStatus = EXIT_IO_ERROR;
2142                 }
2143                 return;
2144             }
2145         }
2146         mExitStatus = EXIT_DONE;
2147     }
2148 
setupService()2149     protected boolean setupService() {
2150         // Make sure account and mailbox are always the latest from the database
2151         mAccount = Account.restoreAccountWithId(mContext, mAccount.mId);
2152         if (mAccount == null) return false;
2153         mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId);
2154         if (mMailbox == null) return false;
2155         mThread = Thread.currentThread();
2156         android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
2157         TAG = mThread.getName();
2158 
2159         HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv);
2160         if (ha == null) return false;
2161         mHostAddress = ha.mAddress;
2162         mUserName = ha.mLogin;
2163         mPassword = ha.mPassword;
2164 
2165         // Set up our protocol version from the Account
2166         mProtocolVersion = mAccount.mProtocolVersion;
2167         // If it hasn't been set up, start with default version
2168         if (mProtocolVersion == null) {
2169             mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION;
2170         }
2171         mProtocolVersionDouble = Double.parseDouble(mProtocolVersion);
2172         return true;
2173     }
2174 
2175     /* (non-Javadoc)
2176      * @see java.lang.Runnable#run()
2177      */
run()2178     public void run() {
2179         if (!setupService()) return;
2180 
2181         try {
2182             SyncManager.callback().syncMailboxStatus(mMailboxId, EmailServiceStatus.IN_PROGRESS, 0);
2183         } catch (RemoteException e1) {
2184             // Don't care if this fails
2185         }
2186 
2187         // Whether or not we're the account mailbox
2188         try {
2189             mDeviceId = SyncManager.getDeviceId();
2190             if ((mMailbox == null) || (mAccount == null)) {
2191                 return;
2192             } else if (mMailbox.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
2193                 runAccountMailbox();
2194             } else {
2195                 AbstractSyncAdapter target;
2196                 if (mMailbox.mType == Mailbox.TYPE_CONTACTS) {
2197                     target = new ContactsSyncAdapter(mMailbox, this);
2198                 } else if (mMailbox.mType == Mailbox.TYPE_CALENDAR) {
2199                     target = new CalendarSyncAdapter(mMailbox, this);
2200                 } else {
2201                     target = new EmailSyncAdapter(mMailbox, this);
2202                 }
2203                 // We loop here because someone might have put a request in while we were syncing
2204                 // and we've missed that opportunity...
2205                 do {
2206                     if (mRequestTime != 0) {
2207                         userLog("Looping for user request...");
2208                         mRequestTime = 0;
2209                     }
2210                     sync(target);
2211                 } while (mRequestTime != 0);
2212             }
2213         } catch (EasAuthenticationException e) {
2214             userLog("Caught authentication error");
2215             mExitStatus = EXIT_LOGIN_FAILURE;
2216         } catch (IOException e) {
2217             String message = e.getMessage();
2218             userLog("Caught IOException: ", (message == null) ? "No message" : message);
2219             mExitStatus = EXIT_IO_ERROR;
2220         } catch (Exception e) {
2221             userLog("Uncaught exception in EasSyncService", e);
2222         } finally {
2223             int status;
2224 
2225             if (!mStop) {
2226                 userLog("Sync finished");
2227                 SyncManager.done(this);
2228                 switch (mExitStatus) {
2229                     case EXIT_IO_ERROR:
2230                         status = EmailServiceStatus.CONNECTION_ERROR;
2231                         break;
2232                     case EXIT_DONE:
2233                         status = EmailServiceStatus.SUCCESS;
2234                         ContentValues cv = new ContentValues();
2235                         cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
2236                         String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount;
2237                         cv.put(Mailbox.SYNC_STATUS, s);
2238                         mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI,
2239                                 mMailboxId), cv, null, null);
2240                         break;
2241                     case EXIT_LOGIN_FAILURE:
2242                         status = EmailServiceStatus.LOGIN_FAILED;
2243                         break;
2244                     case EXIT_SECURITY_FAILURE:
2245                         status = EmailServiceStatus.SECURITY_FAILURE;
2246                         // Ask for a new folder list.  This should wake up the account mailbox; a
2247                         // security error in account mailbox should start the provisioning process
2248                         SyncManager.reloadFolderList(mContext, mAccount.mId, true);
2249                         break;
2250                     default:
2251                         status = EmailServiceStatus.REMOTE_EXCEPTION;
2252                         errorLog("Sync ended due to an exception.");
2253                         break;
2254                 }
2255             } else {
2256                 userLog("Stopped sync finished.");
2257                 status = EmailServiceStatus.SUCCESS;
2258             }
2259 
2260             try {
2261                 SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0);
2262             } catch (RemoteException e1) {
2263                 // Don't care if this fails
2264             }
2265 
2266             // Make sure SyncManager knows about this
2267             SyncManager.kick("sync finished");
2268        }
2269     }
2270 }
2271