1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.exchange.eas;
18
19import android.content.Context;
20import android.content.SyncResult;
21import android.os.Bundle;
22
23import com.android.emailcommon.mail.MessagingException;
24import com.android.emailcommon.provider.Account;
25import com.android.emailcommon.provider.HostAuth;
26import com.android.emailcommon.provider.Policy;
27import com.android.emailcommon.service.EmailServiceProxy;
28import com.android.exchange.CommandStatusException;
29import com.android.exchange.EasResponse;
30import com.android.exchange.adapter.FolderSyncParser;
31import com.android.exchange.adapter.Serializer;
32import com.android.exchange.adapter.Tags;
33import com.android.mail.utils.LogUtils;
34
35import org.apache.http.HttpEntity;
36
37import java.io.IOException;
38
39/**
40 * Implements the EAS FolderSync command. We use this both to actually do a folder sync, and also
41 * during account adding flow as a convenient command to validate the account settings (e.g. since
42 * it needs to login and will tell us about provisioning requirements).
43 * TODO: Doing validation here is kind of wonky. There must be a better way.
44 * TODO: Add the use of the Settings command during validation.
45 *
46 * See http://msdn.microsoft.com/en-us/library/ee237648(v=exchg.80).aspx for more details.
47 */
48public class EasFolderSync extends EasOperation {
49
50    /** Result code indicating the sync completed correctly. */
51    public static final int RESULT_OK = 1;
52    /**
53     * Result code indicating that this object was constructed for sync and was asked to validate,
54     * or vice versa.
55     */
56    public static final int RESULT_WRONG_OPERATION = 2;
57
58    // TODO: Eliminate the need for mAccount (requires FolderSyncParser changes).
59    private final Account mAccount;
60
61    /** Indicates whether this object is for validation rather than sync. */
62    private final boolean mStatusOnly;
63
64    /** During validation, this holds the policy we must enforce. */
65    private Policy mPolicy;
66
67    /**
68     * Constructor for actually doing folder sync.
69     * @param context
70     * @param account
71     */
72    public EasFolderSync(final Context context, final Account account) {
73        super(context, account);
74        mAccount = account;
75        mStatusOnly = false;
76        mPolicy = null;
77    }
78
79    /**
80     * Constructor for account validation.
81     * @param context
82     * @param hostAuth
83     */
84    public EasFolderSync(final Context context, final HostAuth hostAuth) {
85        this(context, new Account(), hostAuth);
86    }
87
88    private EasFolderSync(final Context context, final Account account, final HostAuth hostAuth) {
89        super(context, account, hostAuth);
90        mAccount = account;
91        mAccount.mEmailAddress = hostAuth.mLogin;
92        mStatusOnly = true;
93    }
94
95    /**
96     * Perform a folder sync.
97     * @param syncResult The {@link SyncResult} object for this sync operation.
98     * @return A result code, either from above or from the base class.
99     */
100    public int doFolderSync(final SyncResult syncResult) {
101        if (mStatusOnly) {
102            return RESULT_WRONG_OPERATION;
103        }
104        LogUtils.d(LOG_TAG, "Performing sync for account %d", mAccount.mId);
105        return performOperation(syncResult);
106    }
107
108    /**
109     * Perform account validation.
110     * @return The response {@link Bundle} expected by the RPC.
111     */
112    public Bundle validate() {
113        final Bundle bundle = new Bundle(3);
114        if (!mStatusOnly) {
115            writeResultCode(bundle, RESULT_OTHER_FAILURE);
116            return bundle;
117        }
118        LogUtils.d(LOG_TAG, "Performing validation");
119
120        if (!registerClientCert()) {
121            bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE,
122                    MessagingException.CLIENT_CERTIFICATE_ERROR);
123            return bundle;
124        }
125
126        if (shouldGetProtocolVersion()) {
127            final EasOptions options = new EasOptions(this);
128            final int result = options.getProtocolVersionFromServer(null);
129            if (result != EasOptions.RESULT_OK) {
130                writeResultCode(bundle, result);
131                return bundle;
132            }
133            final String protocolVersion = options.getProtocolVersionString();
134            setProtocolVersion(protocolVersion);
135            bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_PROTOCOL_VERSION, protocolVersion);
136        }
137
138        writeResultCode(bundle, performOperation(null));
139        return bundle;
140    }
141
142    @Override
143    protected String getCommand() {
144        return "FolderSync";
145    }
146
147    @Override
148    protected HttpEntity getRequestEntity() throws IOException {
149        final String syncKey = mAccount.mSyncKey != null ? mAccount.mSyncKey : "0";
150        final Serializer s = new Serializer();
151        s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY).text(syncKey)
152            .end().end().done();
153        return makeEntity(s);
154    }
155
156    @Override
157    protected int handleResponse(final EasResponse response, final SyncResult syncResult)
158            throws IOException, CommandStatusException {
159        if (!response.isEmpty()) {
160            new FolderSyncParser(mContext, mContext.getContentResolver(),
161                    response.getInputStream(), mAccount, mStatusOnly).parse();
162        }
163        return RESULT_OK;
164    }
165
166    @Override
167    protected boolean handleForbidden() {
168        return mStatusOnly;
169    }
170
171    @Override
172    protected boolean handleProvisionError(final SyncResult syncResult, final long accountId) {
173        if (mStatusOnly) {
174            final EasProvision provisionOperation = new EasProvision(this);
175            mPolicy = provisionOperation.test();
176            // Regardless of whether the policy is supported, we return false because there's
177            // no need to re-run the operation.
178            return false;
179        }
180        return super.handleProvisionError(syncResult, accountId);
181    }
182
183    /**
184     * Translate {@link EasOperation} result codes to the values needed by the RPC, and write
185     * them to the {@link Bundle}.
186     * @param bundle The {@link Bundle} to return to the RPC.
187     * @param resultCode The result code for this operation.
188     */
189    private void writeResultCode(final Bundle bundle, final int resultCode) {
190        final int messagingExceptionCode;
191        switch (resultCode) {
192            case RESULT_ABORT:
193                messagingExceptionCode = MessagingException.IOERROR;
194                break;
195            case RESULT_RESTART:
196                messagingExceptionCode = MessagingException.IOERROR;
197                break;
198            case RESULT_TOO_MANY_REDIRECTS:
199                messagingExceptionCode = MessagingException.UNSPECIFIED_EXCEPTION;
200                break;
201            case RESULT_REQUEST_FAILURE:
202                messagingExceptionCode = MessagingException.IOERROR;
203                break;
204            case RESULT_FORBIDDEN:
205                messagingExceptionCode = MessagingException.ACCESS_DENIED;
206                break;
207            case RESULT_PROVISIONING_ERROR:
208                if (mPolicy == null) {
209                    messagingExceptionCode = MessagingException.UNSPECIFIED_EXCEPTION;
210                } else {
211                    bundle.putParcelable(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET, mPolicy);
212                    messagingExceptionCode = mPolicy.mProtocolPoliciesUnsupported == null ?
213                            MessagingException.SECURITY_POLICIES_REQUIRED :
214                            MessagingException.SECURITY_POLICIES_UNSUPPORTED;
215                }
216                break;
217            case RESULT_AUTHENTICATION_ERROR:
218                messagingExceptionCode = MessagingException.AUTHENTICATION_FAILED;
219                break;
220            case RESULT_CLIENT_CERTIFICATE_REQUIRED:
221                messagingExceptionCode = MessagingException.CLIENT_CERTIFICATE_REQUIRED;
222                break;
223            case RESULT_PROTOCOL_VERSION_UNSUPPORTED:
224                messagingExceptionCode = MessagingException.PROTOCOL_VERSION_UNSUPPORTED;
225                break;
226            case RESULT_OTHER_FAILURE:
227                messagingExceptionCode = MessagingException.UNSPECIFIED_EXCEPTION;
228                break;
229            case RESULT_OK:
230                messagingExceptionCode = MessagingException.NO_ERROR;
231                break;
232            default:
233                messagingExceptionCode = MessagingException.UNSPECIFIED_EXCEPTION;
234                break;
235        }
236        bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, messagingExceptionCode);
237    }
238}
239