Folder.java revision 68f83843e821d9627a53f68244dbc4cb26662edc
1/******************************************************************************* 2 * Copyright (C) 2012 Google Inc. 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.mail.providers; 19 20import android.content.Context; 21import android.content.CursorLoader; 22import android.database.Cursor; 23import android.graphics.drawable.PaintDrawable; 24import android.net.Uri; 25import android.net.Uri.Builder; 26import android.os.Parcel; 27import android.os.Parcelable; 28import android.provider.BaseColumns; 29import android.text.TextUtils; 30import android.view.View; 31import android.widget.ImageView; 32 33import com.android.mail.providers.UIProvider.FolderColumns; 34import com.android.mail.utils.LogTag; 35import com.android.mail.utils.LogUtils; 36 37import com.google.common.base.Objects; 38import com.google.common.collect.ImmutableList; 39import com.google.common.collect.Lists; 40import com.google.common.collect.Maps; 41 42import org.json.JSONArray; 43import org.json.JSONException; 44import org.json.JSONObject; 45 46import java.util.ArrayList; 47import java.util.Collection; 48import java.util.Collections; 49import java.util.HashMap; 50import java.util.Map; 51 52/** 53 * A folder is a collection of conversations, and perhaps other folders. 54 */ 55public class Folder implements Parcelable, Comparable<Folder> { 56 /** 57 * 58 */ 59 private static final String FOLDER_UNINITIALIZED = "Uninitialized!"; 60 61 // Try to match the order of members with the order of constants in UIProvider. 62 63 /** 64 * Unique id of this folder. 65 */ 66 public int id; 67 68 /** 69 * The content provider URI that returns this folder for this account. 70 */ 71 public Uri uri; 72 73 /** 74 * The human visible name for this folder. 75 */ 76 public String name; 77 78 /** 79 * The possible capabilities that this folder supports. 80 */ 81 public int capabilities; 82 83 /** 84 * Whether or not this folder has children folders. 85 */ 86 public boolean hasChildren; 87 88 /** 89 * How large the synchronization window is: how many days worth of data is retained on the 90 * device. 91 */ 92 public int syncWindow; 93 94 /** 95 * The content provider URI to return the list of conversations in this 96 * folder. 97 */ 98 public Uri conversationListUri; 99 100 /** 101 * The content provider URI to return the list of child folders of this folder. 102 */ 103 public Uri childFoldersListUri; 104 105 /** 106 * The number of messages that are unread in this folder. 107 */ 108 public int unreadCount; 109 110 /** 111 * The total number of messages in this folder. 112 */ 113 public int totalCount; 114 115 /** 116 * The content provider URI to force a refresh of this folder. 117 */ 118 public Uri refreshUri; 119 120 /** 121 * The current sync status of the folder 122 */ 123 public int syncStatus; 124 125 /** 126 * The result of the last sync for this folder 127 */ 128 public int lastSyncResult; 129 130 /** 131 * Folder type. 0 is default. 132 */ 133 public int type; 134 135 /** 136 * Icon for this folder; 0 implies no icon. 137 */ 138 public long iconResId; 139 140 public String bgColor; 141 public String fgColor; 142 143 /** 144 * The content provider URI to request additional conversations 145 */ 146 public Uri loadMoreUri; 147 148 /** 149 * The possibly empty name of this folder with full hierarchy. 150 * The expected format is: parent/folder1/folder2/folder3/folder4 151 */ 152 public String hierarchicalDesc; 153 154 /** 155 * Parent folder of this folder, or null if there is none. This is set as 156 * part of the execution of the application and not obtained or stored via 157 * the provider. 158 */ 159 public Folder parent; 160 161 /** 162 * Used only for debugging. 163 */ 164 private static final String LOG_TAG = LogTag.getLogTag(); 165 166 /** An immutable, empty conversation list */ 167 public static final Collection<Folder> EMPTY = Collections.emptyList(); 168 169 private static final String FOLDER_PARENT = "folderParent"; 170 171 172 public Folder(Parcel in) { 173 id = in.readInt(); 174 uri = in.readParcelable(null); 175 name = in.readString(); 176 capabilities = in.readInt(); 177 // 1 for true, 0 for false. 178 hasChildren = in.readInt() == 1; 179 syncWindow = in.readInt(); 180 conversationListUri = in.readParcelable(null); 181 childFoldersListUri = in.readParcelable(null); 182 unreadCount = in.readInt(); 183 totalCount = in.readInt(); 184 refreshUri = in.readParcelable(null); 185 syncStatus = in.readInt(); 186 lastSyncResult = in.readInt(); 187 type = in.readInt(); 188 iconResId = in.readLong(); 189 bgColor = in.readString(); 190 fgColor = in.readString(); 191 loadMoreUri = in.readParcelable(null); 192 hierarchicalDesc = in.readString(); 193 parent = in.readParcelable(null); 194 } 195 196 public Folder(Cursor cursor) { 197 id = cursor.getInt(UIProvider.FOLDER_ID_COLUMN); 198 uri = Uri.parse(cursor.getString(UIProvider.FOLDER_URI_COLUMN)); 199 name = cursor.getString(UIProvider.FOLDER_NAME_COLUMN); 200 capabilities = cursor.getInt(UIProvider.FOLDER_CAPABILITIES_COLUMN); 201 // 1 for true, 0 for false. 202 hasChildren = cursor.getInt(UIProvider.FOLDER_HAS_CHILDREN_COLUMN) == 1; 203 syncWindow = cursor.getInt(UIProvider.FOLDER_SYNC_WINDOW_COLUMN); 204 String convList = cursor.getString(UIProvider.FOLDER_CONVERSATION_LIST_URI_COLUMN); 205 conversationListUri = !TextUtils.isEmpty(convList) ? Uri.parse(convList) : null; 206 String childList = cursor.getString(UIProvider.FOLDER_CHILD_FOLDERS_LIST_COLUMN); 207 childFoldersListUri = (hasChildren && !TextUtils.isEmpty(childList)) ? Uri.parse(childList) 208 : null; 209 unreadCount = cursor.getInt(UIProvider.FOLDER_UNREAD_COUNT_COLUMN); 210 totalCount = cursor.getInt(UIProvider.FOLDER_TOTAL_COUNT_COLUMN); 211 String refresh = cursor.getString(UIProvider.FOLDER_REFRESH_URI_COLUMN); 212 refreshUri = !TextUtils.isEmpty(refresh) ? Uri.parse(refresh) : null; 213 syncStatus = cursor.getInt(UIProvider.FOLDER_SYNC_STATUS_COLUMN); 214 lastSyncResult = cursor.getInt(UIProvider.FOLDER_LAST_SYNC_RESULT_COLUMN); 215 type = cursor.getInt(UIProvider.FOLDER_TYPE_COLUMN); 216 iconResId = cursor.getLong(UIProvider.FOLDER_ICON_RES_ID_COLUMN); 217 bgColor = cursor.getString(UIProvider.FOLDER_BG_COLOR_COLUMN); 218 fgColor = cursor.getString(UIProvider.FOLDER_FG_COLOR_COLUMN); 219 String loadMore = cursor.getString(UIProvider.FOLDER_LOAD_MORE_URI_COLUMN); 220 loadMoreUri = !TextUtils.isEmpty(loadMore) ? Uri.parse(loadMore) : null; 221 hierarchicalDesc = cursor.getString(UIProvider.FOLDER_HIERARCHICAL_DESC_COLUMN); 222 parent = null; 223 } 224 225 @Override 226 public void writeToParcel(Parcel dest, int flags) { 227 dest.writeInt(id); 228 dest.writeParcelable(uri, 0); 229 dest.writeString(name); 230 dest.writeInt(capabilities); 231 // 1 for true, 0 for false. 232 dest.writeInt(hasChildren ? 1 : 0); 233 dest.writeInt(syncWindow); 234 dest.writeParcelable(conversationListUri, 0); 235 dest.writeParcelable(childFoldersListUri, 0); 236 dest.writeInt(unreadCount); 237 dest.writeInt(totalCount); 238 dest.writeParcelable(refreshUri, 0); 239 dest.writeInt(syncStatus); 240 dest.writeInt(lastSyncResult); 241 dest.writeInt(type); 242 dest.writeLong(iconResId); 243 dest.writeString(bgColor); 244 dest.writeString(fgColor); 245 dest.writeParcelable(loadMoreUri, 0); 246 dest.writeString(hierarchicalDesc); 247 dest.writeParcelable(parent, 0); 248 } 249 250 /** 251 * Construct a folder that queries for search results. Do not call on the UI 252 * thread. 253 */ 254 public static CursorLoader forSearchResults(Account account, String query, Context context) { 255 if (account.searchUri != null) { 256 Builder searchBuilder = account.searchUri.buildUpon(); 257 searchBuilder.appendQueryParameter(UIProvider.SearchQueryParameters.QUERY, query); 258 Uri searchUri = searchBuilder.build(); 259 return new CursorLoader(context, searchUri, UIProvider.FOLDERS_PROJECTION, null, null, 260 null); 261 } 262 return null; 263 } 264 265 public static ArrayList<Folder> forDisplay(Folder ignoreFolder, String foldersString) { 266 final ArrayList<Folder> folders = Lists.newArrayList(); 267 if (foldersString == null) { 268 return folders; 269 } 270 try { 271 JSONArray array = new JSONArray(foldersString); 272 Folder folder; 273 for (int i = 0; i < array.length(); i++) { 274 folder = new Folder(array.getJSONObject(i)); 275 if (TextUtils.isEmpty(folder.name) 276 || (ignoreFolder != null && ignoreFolder.equals(folder)) 277 || Folder.isProviderFolder(folder)) { 278 continue; 279 } 280 folders.add(folder); 281 } 282 } catch (JSONException e) { 283 LogUtils.wtf(LOG_TAG, e, "Unable to create list of folders from serialzied jsonarray"); 284 } 285 return folders; 286 } 287 288 public static HashMap<Uri, Folder> hashMapForFolders(ArrayList<Folder> rawFolders) { 289 final HashMap<Uri, Folder> folders = new HashMap<Uri, Folder>(); 290 for (Folder f : rawFolders) { 291 folders.put(f.uri, f); 292 } 293 return folders; 294 } 295 296 /** 297 * Return a serialized String for this account. 298 */ 299 public synchronized String serialize() { 300 return toJSON().toString(); 301 } 302 303 public synchronized JSONObject toJSON() { 304 JSONObject json = new JSONObject(); 305 try { 306 json.put(BaseColumns._ID, id); 307 json.put(FolderColumns.URI, uri); 308 json.put(FolderColumns.NAME, name); 309 json.put(FolderColumns.HAS_CHILDREN, hasChildren); 310 json.put(FolderColumns.CAPABILITIES, capabilities); 311 json.put(FolderColumns.SYNC_WINDOW, syncWindow); 312 json.putOpt(FolderColumns.CONVERSATION_LIST_URI, conversationListUri); 313 json.putOpt(FolderColumns.CHILD_FOLDERS_LIST_URI, childFoldersListUri); 314 json.put(FolderColumns.UNREAD_COUNT, unreadCount); 315 json.put(FolderColumns.TOTAL_COUNT, totalCount); 316 json.putOpt(FolderColumns.REFRESH_URI, refreshUri); 317 json.put(FolderColumns.SYNC_STATUS, syncStatus); 318 json.put(FolderColumns.LAST_SYNC_RESULT, lastSyncResult); 319 json.put(FolderColumns.TYPE, type); 320 json.putOpt(FolderColumns.ICON_RES_ID, iconResId); 321 json.putOpt(FolderColumns.BG_COLOR, bgColor); 322 json.putOpt(FolderColumns.FG_COLOR, fgColor); 323 json.putOpt(FolderColumns.LOAD_MORE_URI, loadMoreUri); 324 json.putOpt(FolderColumns.HIERARCHICAL_DESC, hierarchicalDesc); 325 if (parent != null) { 326 json.put(FOLDER_PARENT, parent.toJSON()); 327 } 328 } catch (JSONException e) { 329 LogUtils.wtf(LOG_TAG, e, "Could not serialize account with name %s", name); 330 } 331 return json; 332 } 333 334 /** 335 * Create a new folder from a string representation of JSON. 336 * @throws JSONException 337 */ 338 public static Folder fromJSONString(String in) throws JSONException { 339 return new Folder(new JSONObject(in)); 340 } 341 342 public Folder(JSONObject o) { 343 try { 344 id = o.getInt(BaseColumns._ID); 345 uri = getValidUri(o.getString(FolderColumns.URI)); 346 name = o.getString(FolderColumns.NAME); 347 hasChildren = o.getBoolean(FolderColumns.HAS_CHILDREN); 348 capabilities = o.getInt(FolderColumns.CAPABILITIES); 349 syncWindow = o.getInt(FolderColumns.SYNC_WINDOW); 350 conversationListUri = getValidUri(o.optString(FolderColumns.CONVERSATION_LIST_URI)); 351 childFoldersListUri = getValidUri(o.optString(FolderColumns.CHILD_FOLDERS_LIST_URI)); 352 unreadCount = o.getInt(FolderColumns.UNREAD_COUNT); 353 totalCount = o.getInt(FolderColumns.TOTAL_COUNT); 354 refreshUri = getValidUri(o.optString(FolderColumns.REFRESH_URI)); 355 syncStatus = o.getInt(FolderColumns.SYNC_STATUS); 356 lastSyncResult = o.getInt(FolderColumns.LAST_SYNC_RESULT); 357 type = o.getInt(FolderColumns.TYPE); 358 iconResId = o.optInt(FolderColumns.ICON_RES_ID); 359 bgColor = o.optString(FolderColumns.BG_COLOR); 360 fgColor = o.optString(FolderColumns.FG_COLOR); 361 loadMoreUri = getValidUri(o.optString(FolderColumns.LOAD_MORE_URI)); 362 hierarchicalDesc = o.optString(FolderColumns.HIERARCHICAL_DESC); 363 JSONObject folderParent = o.optJSONObject(FOLDER_PARENT); 364 if (folderParent != null) { 365 parent = new Folder(folderParent); 366 } 367 } catch (JSONException e) { 368 LogUtils.wtf(LOG_TAG, e, "Unable to parse folder from jsonobject"); 369 } 370 } 371 372 private static Uri getValidUri(String uri) { 373 if (TextUtils.isEmpty(uri)) { 374 return null; 375 } 376 return Uri.parse(uri); 377 } 378 379 /** 380 * Constructor that leaves everything uninitialized. For use only by {@link #serialize()} 381 * which is responsible for filling in all the fields 382 */ 383 public Folder() { 384 name = FOLDER_UNINITIALIZED; 385 } 386 387 @SuppressWarnings("hiding") 388 public static final Creator<Folder> CREATOR = new Creator<Folder>() { 389 @Override 390 public Folder createFromParcel(Parcel source) { 391 return new Folder(source); 392 } 393 394 @Override 395 public Folder[] newArray(int size) { 396 return new Folder[size]; 397 } 398 }; 399 400 @Override 401 public int describeContents() { 402 // Return a sort of version number for this parcelable folder. Starting with zero. 403 return 0; 404 } 405 406 @Override 407 public boolean equals(Object o) { 408 if (o == null || !(o instanceof Folder)) { 409 return false; 410 } 411 return Objects.equal(uri, ((Folder) o).uri); 412 } 413 414 @Override 415 public int hashCode() { 416 return uri == null ? 0 : uri.hashCode(); 417 } 418 419 @Override 420 public int compareTo(Folder other) { 421 return name.compareToIgnoreCase(other.name); 422 } 423 424 /** 425 * Create a Folder map from a string of serialized folders. This can only be done on the output 426 * of {@link #serialize(Map)}. 427 * @param serializedFolder A string obtained from {@link #serialize(Map)} 428 * @return a Map of folder name to folder. 429 */ 430 public static Map<String, Folder> parseFoldersFromString(String serializedFolder) { 431 LogUtils.d(LOG_TAG, "folder query result: %s", serializedFolder); 432 433 Map<String, Folder> folderMap = Maps.newHashMap(); 434 if (serializedFolder == null || serializedFolder == "") { 435 return folderMap; 436 } 437 JSONArray folderPieces; 438 try { 439 folderPieces = new JSONArray(serializedFolder); 440 for (int i = 0, n = folderPieces.length(); i < n; i++) { 441 Folder folder = new Folder(folderPieces.getJSONObject(i)); 442 if (folder.name != FOLDER_UNINITIALIZED) { 443 folderMap.put(folder.name, folder); 444 } 445 } 446 } catch (JSONException e) { 447 LogUtils.wtf(LOG_TAG, e, "Unable to parse foldermap from serialized jsonarray"); 448 } 449 return folderMap; 450 } 451 452 /** 453 * Returns a boolean indicating whether network activity (sync) is occuring for this folder. 454 */ 455 public boolean isSyncInProgress() { 456 return 0 != (syncStatus & (UIProvider.SyncStatus.BACKGROUND_SYNC | 457 UIProvider.SyncStatus.USER_REFRESH | 458 UIProvider.SyncStatus.USER_QUERY | 459 UIProvider.SyncStatus.USER_MORE_RESULTS)); 460 } 461 462 /** 463 * Serialize the given list of folders 464 * @param folderMap A valid map of folder names to Folders 465 * @return a string containing a serialized output of folder maps. 466 */ 467 public static String serialize(Map<String, Folder> folderMap) { 468 Collection<Folder> folderCollection = folderMap.values(); 469 Folder[] folderList = folderCollection.toArray(new Folder[]{} ); 470 int numFolders = folderList.length; 471 JSONArray result = new JSONArray(); 472 for (int i = 0; i < numFolders; i++) { 473 result.put(folderList[i].toJSON()); 474 } 475 return result.toString(); 476 } 477 478 public boolean supportsCapability(int capability) { 479 return (capabilities & capability) != 0; 480 } 481 482 // Show black text on a transparent swatch for system folders, effectively hiding the 483 // swatch (see bug 2431925). 484 public static void setFolderBlockColor(Folder folder, View colorBlock) { 485 if (colorBlock == null) { 486 return; 487 } 488 final boolean showBg = !TextUtils.isEmpty(folder.bgColor); 489 final int backgroundColor = showBg ? Integer.parseInt(folder.bgColor) : 0; 490 if (!showBg) { 491 colorBlock.setBackgroundDrawable(null); 492 colorBlock.setVisibility(View.GONE); 493 } else { 494 PaintDrawable paintDrawable = new PaintDrawable(); 495 paintDrawable.getPaint().setColor(backgroundColor); 496 colorBlock.setBackgroundDrawable(paintDrawable); 497 colorBlock.setVisibility(View.VISIBLE); 498 } 499 } 500 501 public static void setIcon(Folder folder, ImageView iconView) { 502 if (iconView == null) { 503 return; 504 } 505 final long icon = folder.iconResId; 506 if (icon > 0) { 507 iconView.setImageResource((int)icon); 508 iconView.setVisibility(View.VISIBLE); 509 } else { 510 iconView.setVisibility(View.INVISIBLE); 511 } 512 } 513 514 /** 515 * Return if the type of the folder matches a provider defined folder. 516 */ 517 public static boolean isProviderFolder(Folder folder) { 518 int type = folder.type; 519 return type == UIProvider.FolderType.INBOX || 520 type == UIProvider.FolderType.DRAFT || 521 type == UIProvider.FolderType.OUTBOX || 522 type == UIProvider.FolderType.SENT || 523 type == UIProvider.FolderType.TRASH || 524 type == UIProvider.FolderType.SPAM; 525 } 526 527 public int getBackgroundColor(int defaultColor) { 528 return TextUtils.isEmpty(bgColor) ? defaultColor : Integer.parseInt(bgColor); 529 } 530 531 public int getForegroundColor(int defaultColor) { 532 return TextUtils.isEmpty(fgColor) ? defaultColor : Integer.parseInt(fgColor); 533 } 534 535 public static String getSerializedFolderString(Collection<Folder> folders) { 536 final JSONArray folderList = new JSONArray(); 537 for (Folder folderEntry : folders) { 538 folderList.put(folderEntry.toJSON()); 539 } 540 return folderList.toString(); 541 } 542 543 /** 544 * Returns a comma separated list of folder URIs for all the folders in the collection. 545 * @param folders 546 * @return 547 */ 548 public final static String getUriString(Collection<Folder> folders) { 549 final StringBuilder uris = new StringBuilder(); 550 boolean first = true; 551 for (Folder f : folders) { 552 if (first) { 553 first = false; 554 } else { 555 uris.append(','); 556 } 557 uris.append(f.uri.toString()); 558 } 559 return uris.toString(); 560 } 561 562 563 /** 564 * Get an array of folders from a rawFolders string. 565 */ 566 public static ArrayList<Folder> getFoldersArray(String rawFolders) { 567 JSONArray folderList; 568 ArrayList<Folder> folders = new ArrayList<Folder>(); 569 try { 570 folderList = new JSONArray(rawFolders); 571 for (int i = 0; i < folderList.length(); i++) { 572 folders.add(new Folder(folderList.getJSONObject(i))); 573 } 574 } catch (JSONException e) { 575 LogUtils.d(LOG_TAG, e, "Error parsing raw folders"); 576 } 577 return folders; 578 } 579 580 /** 581 * Get just the uri's from an arraylist of folders. 582 */ 583 public final static String[] getUriArray(ArrayList<Folder> folders) { 584 String[] folderUris = new String[folders.size()]; 585 int i = 0; 586 for (Folder folder : folders) { 587 folderUris[i] = folder.uri.toString(); 588 i++; 589 } 590 return folderUris; 591 } 592 593 /** 594 * Returns true if a conversation assigned to the needle will be assigned to the collection of 595 * folders in the haystack. False otherwise. This method is safe to call with null 596 * arguments. 597 * This method returns true under two circumstances 598 * <ul><li> If the URI of the needle was found in the collection of URIs that comprise the 599 * haystack. 600 * </li><li> If the needle is of the type Inbox, and at least one of the folders in the haystack 601 * are of type Inbox. <em>Rationale</em>: there are special folders that are marked as inbox, 602 * and the user might not have the control to assign conversations to them. This happens for 603 * the Priority Inbox in Gmail. When you assign a conversation to an Inbox folder, it will 604 * continue to appear in the Priority Inbox. However, the URI of Priority Inbox and Inbox will 605 * be different. So a direct equality check is insufficient. 606 * </li></ul> 607 * @param haystack a collection of folders, possibly overlapping 608 * @param needle a folder 609 * @return true if a conversation inside the needle will be in the folders in the haystack. 610 */ 611 public final static boolean containerIncludes(Collection<Folder> haystack, Folder needle) { 612 // If the haystack is empty, it cannot contain anything. 613 if (haystack == null || haystack.size() <= 0) { 614 return false; 615 } 616 // The null folder exists everywhere. 617 if (needle == null) { 618 return true; 619 } 620 boolean hasInbox = false; 621 // Get currently active folder info and compare it to the list 622 // these conversations have been given; if they no longer contain 623 // the selected folder, delete them from the list. 624 final Uri toFind = needle.uri; 625 for (Folder f : haystack) { 626 if (toFind.equals(f.uri)) { 627 return true; 628 } 629 hasInbox |= (f.type == UIProvider.FolderType.INBOX); 630 } 631 // Did not find the URI of needle directly. If the needle is an Inbox and one of the folders 632 // was an inbox, then the needle is contained (check Javadoc for explanation). 633 final boolean needleIsInbox = (needle.type == UIProvider.FolderType.INBOX); 634 return needleIsInbox ? hasInbox : false; 635 } 636 637 /** 638 * Returns a collection of a single folder. This method always returns a valid collection 639 * even if the input folder is null. 640 * @param in a folder, possibly null. 641 * @return a collection of the folder. 642 */ 643 public static Collection<Folder> listOf(Folder in) { 644 final Collection<Folder> target = (in == null) ? EMPTY : ImmutableList.of(in); 645 return target; 646 } 647}