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.adapter; 19 20 import com.android.email.provider.AttachmentProvider; 21 import com.android.email.provider.EmailContent; 22 import com.android.email.provider.EmailProvider; 23 import com.android.email.provider.EmailContent.AccountColumns; 24 import com.android.email.provider.EmailContent.Mailbox; 25 import com.android.email.provider.EmailContent.MailboxColumns; 26 import com.android.exchange.Eas; 27 import com.android.exchange.MockParserStream; 28 import com.android.exchange.SyncManager; 29 30 import android.content.ContentProviderOperation; 31 import android.content.ContentUris; 32 import android.content.ContentValues; 33 import android.content.OperationApplicationException; 34 import android.database.Cursor; 35 import android.os.RemoteException; 36 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.util.ArrayList; 40 import java.util.Arrays; 41 import java.util.List; 42 43 /** 44 * Parse the result of a FolderSync command 45 * 46 * Handles the addition, deletion, and changes to folders in the user's Exchange account. 47 **/ 48 49 public class FolderSyncParser extends AbstractSyncParser { 50 51 public static final String TAG = "FolderSyncParser"; 52 53 // These are defined by the EAS protocol 54 public static final int USER_FOLDER_TYPE = 1; 55 public static final int INBOX_TYPE = 2; 56 public static final int DRAFTS_TYPE = 3; 57 public static final int DELETED_TYPE = 4; 58 public static final int SENT_TYPE = 5; 59 public static final int OUTBOX_TYPE = 6; 60 public static final int TASKS_TYPE = 7; 61 public static final int CALENDAR_TYPE = 8; 62 public static final int CONTACTS_TYPE = 9; 63 public static final int NOTES_TYPE = 10; 64 public static final int JOURNAL_TYPE = 11; 65 public static final int USER_MAILBOX_TYPE = 12; 66 67 public static final List<Integer> mValidFolderTypes = Arrays.asList(INBOX_TYPE, DRAFTS_TYPE, 68 DELETED_TYPE, SENT_TYPE, OUTBOX_TYPE, USER_MAILBOX_TYPE, CALENDAR_TYPE, CONTACTS_TYPE); 69 70 public static final String ALL_BUT_ACCOUNT_MAILBOX = MailboxColumns.ACCOUNT_KEY + "=? and " + 71 MailboxColumns.TYPE + "!=" + Mailbox.TYPE_EAS_ACCOUNT_MAILBOX; 72 73 private static final String WHERE_SERVER_ID_AND_ACCOUNT = MailboxColumns.SERVER_ID + "=? and " + 74 MailboxColumns.ACCOUNT_KEY + "=?"; 75 76 private static final String WHERE_DISPLAY_NAME_AND_ACCOUNT = MailboxColumns.DISPLAY_NAME + 77 "=? and " + MailboxColumns.ACCOUNT_KEY + "=?"; 78 79 private static final String WHERE_PARENT_SERVER_ID_AND_ACCOUNT = 80 MailboxColumns.PARENT_SERVER_ID +"=? and " + MailboxColumns.ACCOUNT_KEY + "=?"; 81 82 private static final String[] MAILBOX_ID_COLUMNS_PROJECTION = 83 new String[] {MailboxColumns.ID, MailboxColumns.SERVER_ID}; 84 85 private long mAccountId; 86 private String mAccountIdAsString; 87 private MockParserStream mMock = null; 88 private String[] mBindArguments = new String[2]; 89 90 public FolderSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException { 91 super(in, adapter); 92 mAccountId = mAccount.mId; 93 mAccountIdAsString = Long.toString(mAccountId); 94 if (in instanceof MockParserStream) { 95 mMock = (MockParserStream)in; 96 } 97 } 98 99 @Override 100 public boolean parse() throws IOException { 101 int status; 102 boolean res = false; 103 boolean resetFolders = false; 104 if (nextTag(START_DOCUMENT) != Tags.FOLDER_FOLDER_SYNC) 105 throw new EasParserException(); 106 while (nextTag(START_DOCUMENT) != END_DOCUMENT) { 107 if (tag == Tags.FOLDER_STATUS) { 108 status = getValueInt(); 109 if (status != Eas.FOLDER_STATUS_OK) { 110 mService.errorLog("FolderSync failed: " + status); 111 if (status == Eas.FOLDER_STATUS_INVALID_KEY) { 112 mAccount.mSyncKey = "0"; 113 mService.errorLog("Bad sync key; RESET and delete all folders"); 114 mContentResolver.delete(Mailbox.CONTENT_URI, ALL_BUT_ACCOUNT_MAILBOX, 115 new String[] {Long.toString(mAccountId)}); 116 // Stop existing syncs and reconstruct _main 117 SyncManager.stopNonAccountMailboxSyncsForAccount(mAccountId); 118 res = true; 119 resetFolders = true; 120 } else { 121 // Other errors are at the server, so let's throw an error that will 122 // cause this sync to be retried at a later time 123 mService.errorLog("Throwing IOException; will retry later"); 124 throw new EasParserException("Folder status error"); 125 } 126 } 127 } else if (tag == Tags.FOLDER_SYNC_KEY) { 128 mAccount.mSyncKey = getValue(); 129 userLog("New Account SyncKey: ", mAccount.mSyncKey); 130 } else if (tag == Tags.FOLDER_CHANGES) { 131 changesParser(); 132 } else 133 skipTag(); 134 } 135 synchronized (mService.getSynchronizer()) { 136 if (!mService.isStopped() || resetFolders) { 137 ContentValues cv = new ContentValues(); 138 cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey); 139 mAccount.update(mContext, cv); 140 userLog("Leaving FolderSyncParser with Account syncKey=", mAccount.mSyncKey); 141 } 142 } 143 return res; 144 } 145 146 private Cursor getServerIdCursor(String serverId) { 147 mBindArguments[0] = serverId; 148 mBindArguments[1] = mAccountIdAsString; 149 return mContentResolver.query(Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION, 150 WHERE_SERVER_ID_AND_ACCOUNT, mBindArguments, null); 151 } 152 153 public void deleteParser(ArrayList<ContentProviderOperation> ops) throws IOException { 154 while (nextTag(Tags.FOLDER_DELETE) != END) { 155 switch (tag) { 156 case Tags.FOLDER_SERVER_ID: 157 String serverId = getValue(); 158 // Find the mailbox in this account with the given serverId 159 Cursor c = getServerIdCursor(serverId); 160 try { 161 if (c.moveToFirst()) { 162 userLog("Deleting ", serverId); 163 ops.add(ContentProviderOperation.newDelete( 164 ContentUris.withAppendedId(Mailbox.CONTENT_URI, 165 c.getLong(0))).build()); 166 AttachmentProvider.deleteAllMailboxAttachmentFiles(mContext, 167 mAccountId, mMailbox.mId); 168 } 169 } finally { 170 c.close(); 171 } 172 break; 173 default: 174 skipTag(); 175 } 176 } 177 } 178 179 public void addParser(ArrayList<ContentProviderOperation> ops) throws IOException { 180 String name = null; 181 String serverId = null; 182 String parentId = null; 183 int type = 0; 184 185 while (nextTag(Tags.FOLDER_ADD) != END) { 186 switch (tag) { 187 case Tags.FOLDER_DISPLAY_NAME: { 188 name = getValue(); 189 break; 190 } 191 case Tags.FOLDER_TYPE: { 192 type = getValueInt(); 193 break; 194 } 195 case Tags.FOLDER_PARENT_ID: { 196 parentId = getValue(); 197 break; 198 } 199 case Tags.FOLDER_SERVER_ID: { 200 serverId = getValue(); 201 break; 202 } 203 default: 204 skipTag(); 205 } 206 } 207 if (mValidFolderTypes.contains(type)) { 208 Mailbox m = new Mailbox(); 209 m.mDisplayName = name; 210 m.mServerId = serverId; 211 m.mAccountKey = mAccountId; 212 m.mType = Mailbox.TYPE_MAIL; 213 // Note that all mailboxes default to checking "never" (i.e. manual sync only) 214 // We set specific intervals for inbox, contacts, and (eventually) calendar 215 m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER; 216 switch (type) { 217 case INBOX_TYPE: 218 m.mType = Mailbox.TYPE_INBOX; 219 m.mSyncInterval = mAccount.mSyncInterval; 220 break; 221 case CONTACTS_TYPE: 222 m.mType = Mailbox.TYPE_CONTACTS; 223 m.mSyncInterval = mAccount.mSyncInterval; 224 break; 225 case OUTBOX_TYPE: 226 // TYPE_OUTBOX mailboxes are known by SyncManager to sync whenever they aren't 227 // empty. The value of mSyncFrequency is ignored for this kind of mailbox. 228 m.mType = Mailbox.TYPE_OUTBOX; 229 break; 230 case SENT_TYPE: 231 m.mType = Mailbox.TYPE_SENT; 232 break; 233 case DRAFTS_TYPE: 234 m.mType = Mailbox.TYPE_DRAFTS; 235 break; 236 case DELETED_TYPE: 237 m.mType = Mailbox.TYPE_TRASH; 238 break; 239 case CALENDAR_TYPE: 240 m.mType = Mailbox.TYPE_CALENDAR; 241 m.mSyncInterval = mAccount.mSyncInterval; 242 break; 243 } 244 245 // Make boxes like Contacts and Calendar invisible in the folder list 246 m.mFlagVisible = (m.mType < Mailbox.TYPE_NOT_EMAIL); 247 248 if (!parentId.equals("0")) { 249 m.mParentServerId = parentId; 250 } 251 252 userLog("Adding mailbox: ", m.mDisplayName); 253 ops.add(ContentProviderOperation 254 .newInsert(Mailbox.CONTENT_URI).withValues(m.toContentValues()).build()); 255 } 256 257 return; 258 } 259 260 public void updateParser(ArrayList<ContentProviderOperation> ops) throws IOException { 261 String serverId = null; 262 String displayName = null; 263 String parentId = null; 264 while (nextTag(Tags.FOLDER_UPDATE) != END) { 265 switch (tag) { 266 case Tags.FOLDER_SERVER_ID: 267 serverId = getValue(); 268 break; 269 case Tags.FOLDER_DISPLAY_NAME: 270 displayName = getValue(); 271 break; 272 case Tags.FOLDER_PARENT_ID: 273 parentId = getValue(); 274 break; 275 default: 276 skipTag(); 277 break; 278 } 279 } 280 // We'll make a change if one of parentId or displayName are specified 281 // serverId is required, but let's be careful just the same 282 if (serverId != null && (displayName != null || parentId != null)) { 283 Cursor c = getServerIdCursor(serverId); 284 try { 285 // If we find the mailbox (using serverId), make the change 286 if (c.moveToFirst()) { 287 userLog("Updating ", serverId); 288 ContentValues cv = new ContentValues(); 289 if (displayName != null) { 290 cv.put(Mailbox.DISPLAY_NAME, displayName); 291 } 292 if (parentId != null) { 293 cv.put(Mailbox.PARENT_SERVER_ID, parentId); 294 } 295 ops.add(ContentProviderOperation.newUpdate( 296 ContentUris.withAppendedId(Mailbox.CONTENT_URI, 297 c.getLong(0))).withValues(cv).build()); 298 } 299 } finally { 300 c.close(); 301 } 302 } 303 } 304 305 public void changesParser() throws IOException { 306 // Keep track of new boxes, deleted boxes, updated boxes 307 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 308 309 while (nextTag(Tags.FOLDER_CHANGES) != END) { 310 if (tag == Tags.FOLDER_ADD) { 311 addParser(ops); 312 } else if (tag == Tags.FOLDER_DELETE) { 313 deleteParser(ops); 314 } else if (tag == Tags.FOLDER_UPDATE) { 315 updateParser(ops); 316 } else if (tag == Tags.FOLDER_COUNT) { 317 getValueInt(); 318 } else 319 skipTag(); 320 } 321 322 // The mock stream is used for junit tests, so that the parsing code can be tested 323 // separately from the provider code. 324 // TODO Change tests to not require this; remove references to the mock stream 325 if (mMock != null) { 326 mMock.setResult(null); 327 return; 328 } 329 330 // Create the new mailboxes in a single batch operation 331 // Don't save any data if the service has been stopped 332 synchronized (mService.getSynchronizer()) { 333 if (!ops.isEmpty() && !mService.isStopped()) { 334 userLog("Applying ", ops.size(), " mailbox operations."); 335 336 // Execute the batch 337 try { 338 mContentResolver.applyBatch(EmailProvider.EMAIL_AUTHORITY, ops); 339 userLog("New Account SyncKey: ", mAccount.mSyncKey); 340 } catch (RemoteException e) { 341 // There is nothing to be done here; fail by returning null 342 } catch (OperationApplicationException e) { 343 // There is nothing to be done here; fail by returning null 344 } 345 346 // Look for sync issues and its children and delete them 347 // I'm not aware of any other way to deal with this properly 348 mBindArguments[0] = "Sync Issues"; 349 mBindArguments[1] = mAccountIdAsString; 350 Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, 351 MAILBOX_ID_COLUMNS_PROJECTION, WHERE_DISPLAY_NAME_AND_ACCOUNT, 352 mBindArguments, null); 353 String parentServerId = null; 354 long id = 0; 355 try { 356 if (c.moveToFirst()) { 357 id = c.getLong(0); 358 parentServerId = c.getString(1); 359 } 360 } finally { 361 c.close(); 362 } 363 if (parentServerId != null) { 364 mContentResolver.delete(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id), 365 null, null); 366 mBindArguments[0] = parentServerId; 367 mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_PARENT_SERVER_ID_AND_ACCOUNT, 368 mBindArguments); 369 } 370 } 371 } 372 } 373 374 /** 375 * Not needed for FolderSync parsing; everything is done within changesParser 376 */ 377 @Override 378 public void commandsParser() throws IOException { 379 } 380 381 /** 382 * We don't need to implement commit() because all operations take place atomically within 383 * changesParser 384 */ 385 @Override 386 public void commit() throws IOException { 387 } 388 389 @Override 390 public void wipe() { 391 } 392 393 @Override 394 public void responsesParser() throws IOException { 395 } 396 397 } 398