1/*
2 * Copyright (C) 2008-2009 Marc Blank
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.exchange.adapter;
19
20import android.content.ContentResolver;
21import android.content.ContentValues;
22import android.content.Context;
23import android.content.OperationApplicationException;
24import android.os.Bundle;
25import android.os.RemoteException;
26
27import com.android.emailcommon.provider.Account;
28import com.android.emailcommon.provider.EmailContent;
29import com.android.emailcommon.provider.EmailContent.MailboxColumns;
30import com.android.emailcommon.provider.Mailbox;
31import com.android.exchange.CommandStatusException;
32import com.android.exchange.CommandStatusException.CommandStatus;
33import com.android.exchange.Eas;
34import com.android.mail.utils.LogUtils;
35
36import java.io.IOException;
37import java.io.InputStream;
38
39/**
40 * Base class for the Email and PIM sync parsers
41 * Handles the basic flow of syncKeys, looping to get more data, handling errors, etc.
42 * Each subclass must implement a handful of methods that relate specifically to the data type
43 *
44 */
45public abstract class AbstractSyncParser extends Parser {
46    private static final String TAG = Eas.LOG_TAG;
47
48    protected Mailbox mMailbox;
49    protected Account mAccount;
50    protected Context mContext;
51    protected ContentResolver mContentResolver;
52
53    private boolean mLooping;
54
55    public AbstractSyncParser(final Context context, final ContentResolver resolver,
56            final InputStream in, final Mailbox mailbox, final Account account) throws IOException {
57        super(in);
58        init(context, resolver, mailbox, account);
59    }
60
61    public AbstractSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException {
62        super(in);
63        init(adapter);
64    }
65
66    public AbstractSyncParser(Parser p, AbstractSyncAdapter adapter) throws IOException {
67        super(p);
68        init(adapter);
69    }
70
71    public AbstractSyncParser(final Parser p, final Context context, final ContentResolver resolver,
72        final Mailbox mailbox, final Account account) throws IOException {
73        super(p);
74        init(context, resolver, mailbox, account);
75    }
76
77    private void init(final AbstractSyncAdapter adapter) {
78        init(adapter.mContext, adapter.mContext.getContentResolver(), adapter.mMailbox,
79                adapter.mAccount);
80    }
81
82    private void init(final Context context, final ContentResolver resolver, final Mailbox mailbox,
83            final Account account) {
84        mContext = context;
85        mContentResolver = resolver;
86        mMailbox = mailbox;
87        mAccount = account;
88    }
89
90    /**
91     * Read, parse, and act on incoming commands from the Exchange server
92     * @throws IOException if the connection is broken
93     * @throws CommandStatusException
94     */
95    public abstract void commandsParser() throws IOException, CommandStatusException;
96
97    /**
98     * Read, parse, and act on server responses
99     * @throws IOException
100     */
101    public abstract void responsesParser() throws IOException;
102
103    /**
104     * Commit any changes found during parsing
105     * @throws IOException
106     */
107    public abstract void commit() throws IOException, RemoteException,
108            OperationApplicationException;
109
110    public boolean isLooping() {
111        return mLooping;
112    }
113
114    /**
115     * Skip through tags until we reach the specified end tag
116     * @param endTag the tag we end with
117     * @throws IOException
118     */
119    public void skipParser(int endTag) throws IOException {
120        while (nextTag(endTag) != END) {
121            skipTag();
122        }
123    }
124
125    /**
126     * Loop through the top-level structure coming from the Exchange server
127     * Sync keys and the more available flag are handled here, whereas specific data parsing
128     * is handled by abstract methods implemented for each data class (e.g. Email, Contacts, etc.)
129     * @throws CommandStatusException
130     */
131    @Override
132    public boolean parse() throws IOException, CommandStatusException {
133        int status;
134        boolean moreAvailable = false;
135        boolean newSyncKey = false;
136        mLooping = false;
137        // If we're not at the top of the xml tree, throw an exception
138        if (nextTag(START_DOCUMENT) != Tags.SYNC_SYNC) {
139            throw new EasParserException();
140        }
141
142        boolean mailboxUpdated = false;
143        ContentValues cv = new ContentValues();
144
145        // Loop here through the remaining xml
146        while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
147            if (tag == Tags.SYNC_COLLECTION || tag == Tags.SYNC_COLLECTIONS) {
148                // Ignore these tags, since we've only got one collection syncing in this loop
149            } else if (tag == Tags.SYNC_STATUS) {
150                // Status = 1 is success; everything else is a failure
151                status = getValueInt();
152                if (status != 1) {
153                    if (status == 3 || CommandStatus.isBadSyncKey(status)) {
154                        // Must delete all of the data and start over with syncKey of "0"
155                        mMailbox.mSyncKey = "0";
156                        newSyncKey = true;
157                        wipe();
158                        // Indicate there's more so that we'll start syncing again
159                        moreAvailable = true;
160                    } else if (status == 16 || status == 5) {
161                        // Status 16 indicates a transient server error (indeterminate state)
162                        // Status 5 indicates "server error"; this tends to loop for a while so
163                        // throwing IOException will at least provide backoff behavior
164                        throw new IOException();
165                    } else if (status == 8 || status == 12) {
166                        // Status 8 is Bad; it means the server doesn't recognize the serverId it
167                        // sent us.  12 means that we're being asked to refresh the folder list.
168                        // We'll do that with 8 also...
169                        // TODO: Improve this -- probably best to do this synchronously and then
170                        // immediately retry the current sync.
171                        final Bundle extras = new Bundle(1);
172                        extras.putBoolean(Mailbox.SYNC_EXTRA_ACCOUNT_ONLY, true);
173                        ContentResolver.requestSync(new android.accounts.Account(
174                                mAccount.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
175                                EmailContent.AUTHORITY, extras);
176                        // We don't have any provision for telling the user "wait a minute while
177                        // we sync folders"...
178                        throw new IOException();
179                    } else if (status == 7) {
180                        // TODO: Fix this. The handling here used to be pretty bogus, and it's not
181                        // obvious that simply forcing another resync makes sense here.
182                        moreAvailable = true;
183                    } else {
184                        LogUtils.e(LogUtils.TAG, "Sync: Unknown status: " + status);
185                        // Access, provisioning, transient, etc.
186                        throw new CommandStatusException(status);
187                    }
188                }
189            } else if (tag == Tags.SYNC_COMMANDS) {
190                commandsParser();
191            } else if (tag == Tags.SYNC_RESPONSES) {
192                responsesParser();
193            } else if (tag == Tags.SYNC_MORE_AVAILABLE) {
194                moreAvailable = true;
195            } else if (tag == Tags.SYNC_SYNC_KEY) {
196                if (mMailbox.mSyncKey.equals("0")) {
197                    moreAvailable = true;
198                }
199                String newKey = getValue();
200                userLog("Parsed key for ", mMailbox.mDisplayName, ": ", newKey);
201                if (!newKey.equals(mMailbox.mSyncKey)) {
202                    mMailbox.mSyncKey = newKey;
203                    cv.put(MailboxColumns.SYNC_KEY, newKey);
204                    mailboxUpdated = true;
205                    newSyncKey = true;
206                }
207           } else {
208                skipTag();
209           }
210        }
211
212        // If we don't have a new sync key, ignore moreAvailable (or we'll loop)
213        if (moreAvailable && !newSyncKey) {
214            LogUtils.e(TAG, "Looping detected");
215            mLooping = true;
216        }
217
218        // Commit any changes
219        try {
220            commit();
221            if (mailboxUpdated) {
222                mMailbox.update(mContext, cv);
223            }
224        } catch (RemoteException e) {
225            LogUtils.e(TAG, "Failed to commit changes", e);
226        } catch (OperationApplicationException e) {
227            LogUtils.e(TAG, "Failed to commit changes", e);
228        }
229        // Let the caller know that there's more to do
230        if (moreAvailable) {
231            userLog("MoreAvailable");
232        }
233        return moreAvailable;
234    }
235
236    abstract protected void wipe();
237
238    void userLog(String ...strings) {
239        // TODO: Convert to other logging types?
240        //mService.userLog(strings);
241    }
242
243    void userLog(String string, int num, String string2) {
244        // TODO: Convert to other logging types?
245        //mService.userLog(string, num, string2);
246    }
247}
248