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;
23
24import com.android.emailcommon.provider.Account;
25import com.android.emailcommon.provider.EmailContent.MailboxColumns;
26import com.android.emailcommon.provider.Mailbox;
27import com.android.exchange.CommandStatusException;
28import com.android.exchange.CommandStatusException.CommandStatus;
29import com.android.exchange.EasSyncService;
30import com.android.exchange.ExchangeService;
31
32import java.io.IOException;
33import java.io.InputStream;
34
35/**
36 * Base class for the Email and PIM sync parsers
37 * Handles the basic flow of syncKeys, looping to get more data, handling errors, etc.
38 * Each subclass must implement a handful of methods that relate specifically to the data type
39 *
40 */
41public abstract class AbstractSyncParser extends Parser {
42
43    protected EasSyncService mService;
44    protected Mailbox mMailbox;
45    protected Account mAccount;
46    protected Context mContext;
47    protected ContentResolver mContentResolver;
48    protected AbstractSyncAdapter mAdapter;
49
50    private boolean mLooping;
51
52    public AbstractSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException {
53        super(in);
54        init(adapter);
55    }
56
57    public AbstractSyncParser(Parser p, AbstractSyncAdapter adapter) throws IOException {
58        super(p);
59        init(adapter);
60    }
61
62    private void init(AbstractSyncAdapter adapter) {
63        mAdapter = adapter;
64        mService = adapter.mService;
65        mContext = mService.mContext;
66        mContentResolver = mContext.getContentResolver();
67        mMailbox = mService.mMailbox;
68        mAccount = mService.mAccount;
69    }
70
71    /**
72     * Read, parse, and act on incoming commands from the Exchange server
73     * @throws IOException if the connection is broken
74     * @throws CommandStatusException
75     */
76    public abstract void commandsParser() throws IOException, CommandStatusException;
77
78    /**
79     * Read, parse, and act on server responses
80     * @throws IOException
81     */
82    public abstract void responsesParser() throws IOException;
83
84    /**
85     * Commit any changes found during parsing
86     * @throws IOException
87     */
88    public abstract void commit() throws IOException;
89
90    public boolean isLooping() {
91        return mLooping;
92    }
93
94    /**
95     * Skip through tags until we reach the specified end tag
96     * @param endTag the tag we end with
97     * @throws IOException
98     */
99    public void skipParser(int endTag) throws IOException {
100        while (nextTag(endTag) != END) {
101            skipTag();
102        }
103    }
104
105    /**
106     * Loop through the top-level structure coming from the Exchange server
107     * Sync keys and the more available flag are handled here, whereas specific data parsing
108     * is handled by abstract methods implemented for each data class (e.g. Email, Contacts, etc.)
109     * @throws CommandStatusException
110     */
111    @Override
112    public boolean parse() throws IOException, CommandStatusException {
113        int status;
114        boolean moreAvailable = false;
115        boolean newSyncKey = false;
116        int interval = mMailbox.mSyncInterval;
117        mLooping = false;
118        // If we're not at the top of the xml tree, throw an exception
119        if (nextTag(START_DOCUMENT) != Tags.SYNC_SYNC) {
120            throw new EasParserException();
121        }
122
123        boolean mailboxUpdated = false;
124        ContentValues cv = new ContentValues();
125
126        // Loop here through the remaining xml
127        while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
128            if (tag == Tags.SYNC_COLLECTION || tag == Tags.SYNC_COLLECTIONS) {
129                // Ignore these tags, since we've only got one collection syncing in this loop
130            } else if (tag == Tags.SYNC_STATUS) {
131                // Status = 1 is success; everything else is a failure
132                status = getValueInt();
133                if (status != 1) {
134                    mService.errorLog("Sync failed: " + CommandStatus.toString(status));
135                    if (status == 3 || CommandStatus.isBadSyncKey(status)) {
136                        // Must delete all of the data and start over with syncKey of "0"
137                        mAdapter.setSyncKey("0", false);
138                        // Make this a push box through the first sync
139                        // TODO Make frequency conditional on user settings!
140                        mMailbox.mSyncInterval = Mailbox.CHECK_INTERVAL_PUSH;
141                        mService.errorLog("Bad sync key; RESET and delete data");
142                        mAdapter.wipe();
143                        // Indicate there's more so that we'll start syncing again
144                        moreAvailable = true;
145                    } else if (status == 16 || status == 5) {
146                        // Status 16 indicates a transient server error (indeterminate state)
147                        // Status 5 indicates "server error"; this tends to loop for a while so
148                        // throwing IOException will at least provide backoff behavior
149                        throw new IOException();
150                    } else if (status == 8 || status == 12) {
151                        // Status 8 is Bad; it means the server doesn't recognize the serverId it
152                        // sent us.  12 means that we're being asked to refresh the folder list.
153                        // We'll do that with 8 also...
154                        ExchangeService.reloadFolderList(mContext, mAccount.mId, true);
155                        // We don't have any provision for telling the user "wait a minute while
156                        // we sync folders"...
157                        throw new IOException();
158                    } else if (status == 7) {
159                        mService.mUpsyncFailed = true;
160                        moreAvailable = true;
161                    } else {
162                        // Access, provisioning, transient, etc.
163                        throw new CommandStatusException(status);
164                    }
165                }
166            } else if (tag == Tags.SYNC_COMMANDS) {
167                commandsParser();
168            } else if (tag == Tags.SYNC_RESPONSES) {
169                responsesParser();
170            } else if (tag == Tags.SYNC_MORE_AVAILABLE) {
171                moreAvailable = true;
172            } else if (tag == Tags.SYNC_SYNC_KEY) {
173                if (mAdapter.getSyncKey().equals("0")) {
174                    moreAvailable = true;
175                }
176                String newKey = getValue();
177                userLog("Parsed key for ", mMailbox.mDisplayName, ": ", newKey);
178                if (!newKey.equals(mMailbox.mSyncKey)) {
179                    mAdapter.setSyncKey(newKey, true);
180                    cv.put(MailboxColumns.SYNC_KEY, newKey);
181                    mailboxUpdated = true;
182                    newSyncKey = true;
183                }
184                // If we were pushing (i.e. auto-start), now we'll become ping-triggered
185                if (mMailbox.mSyncInterval == Mailbox.CHECK_INTERVAL_PUSH) {
186                    mMailbox.mSyncInterval = Mailbox.CHECK_INTERVAL_PING;
187                }
188           } else {
189                skipTag();
190           }
191        }
192
193        // If we don't have a new sync key, ignore moreAvailable (or we'll loop)
194        if (moreAvailable && !newSyncKey) {
195            mLooping = true;
196        }
197
198        // Commit any changes
199        commit();
200
201        boolean abortSyncs = false;
202
203        // If the sync interval has changed, we need to save it
204        if (mMailbox.mSyncInterval != interval) {
205            cv.put(MailboxColumns.SYNC_INTERVAL, mMailbox.mSyncInterval);
206            mailboxUpdated = true;
207        // If there are changes, and we were bounced from push/ping, try again
208        } else if (mService.mChangeCount > 0 &&
209                mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH &&
210                mMailbox.mSyncInterval > 0) {
211            userLog("Changes found to ping loop mailbox ", mMailbox.mDisplayName, ": will ping.");
212            cv.put(MailboxColumns.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PING);
213            mailboxUpdated = true;
214            abortSyncs = true;
215        }
216
217        if (mailboxUpdated) {
218             synchronized (mService.getSynchronizer()) {
219                if (!mService.isStopped()) {
220                     mMailbox.update(mContext, cv);
221                }
222            }
223        }
224
225        if (abortSyncs) {
226            userLog("Aborting account syncs due to mailbox change to ping...");
227            ExchangeService.stopAccountSyncs(mAccount.mId);
228        }
229
230        // Let the caller know that there's more to do
231        if (moreAvailable) {
232            userLog("MoreAvailable");
233        }
234        return moreAvailable;
235    }
236
237    void userLog(String ...strings) {
238        mService.userLog(strings);
239    }
240
241    void userLog(String string, int num, String string2) {
242        mService.userLog(string, num, string2);
243    }
244}
245