Folder.java revision 41b9e8f7bea47bbcae71b9ae81c3608a00a90e70
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.text.TextUtils; 29import android.view.View; 30import android.widget.ImageView; 31 32import com.android.mail.utils.Utils; 33 34import com.google.common.base.Objects; 35import com.google.common.collect.ImmutableList; 36import com.google.common.collect.Maps; 37 38import java.util.ArrayList; 39import java.util.Collection; 40import java.util.Collections; 41import java.util.HashMap; 42import java.util.Map; 43import java.util.regex.Pattern; 44 45/** 46 * A folder is a collection of conversations, and perhaps other folders. 47 */ 48public class Folder implements Parcelable, Comparable<Folder> { 49 /** 50 * 51 */ 52 private static final String FOLDER_UNINITIALIZED = "Uninitialized!"; 53 54 // TODO: remove this once we figure out which folder is returning a "null" string as the 55 // conversation list uri 56 private static final String NULL_STRING_URI = "null"; 57 58 // Try to match the order of members with the order of constants in UIProvider. 59 60 /** 61 * Unique id of this folder. 62 */ 63 public int id; 64 65 /** 66 * The content provider URI that returns this folder for this account. 67 */ 68 public Uri uri; 69 70 /** 71 * The human visible name for this folder. 72 */ 73 public String name; 74 75 /** 76 * The possible capabilities that this folder supports. 77 */ 78 public int capabilities; 79 80 /** 81 * Whether or not this folder has children folders. 82 */ 83 public boolean hasChildren; 84 85 /** 86 * How large the synchronization window is: how many days worth of data is retained on the 87 * device. 88 */ 89 public int syncWindow; 90 91 /** 92 * The content provider URI to return the list of conversations in this 93 * folder. 94 */ 95 public Uri conversationListUri; 96 97 /** 98 * The content provider URI to return the list of child folders of this folder. 99 */ 100 public Uri childFoldersListUri; 101 102 /** 103 * The number of messages that are unread in this folder. 104 */ 105 public int unreadCount; 106 107 /** 108 * The total number of messages in this folder. 109 */ 110 public int totalCount; 111 112 /** 113 * The content provider URI to force a refresh of this folder. 114 */ 115 public Uri refreshUri; 116 117 /** 118 * The current sync status of the folder 119 */ 120 public int syncStatus; 121 122 /** 123 * A packed integer containing the last synced result, and the request code. The 124 * value is (requestCode << 4) | syncResult 125 * syncResult is a value from {@link UIProvider.LastSyncResult} 126 * requestCode is a value from: {@link UIProvider.SyncStatus}, 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 /** An immutable, empty conversation list */ 162 public static final Collection<Folder> EMPTY = Collections.emptyList(); 163 164 public static final String SPLITTER = "^*^"; 165 private static final Pattern SPLITTER_REGEX = Pattern.compile("\\^\\*\\^"); 166 public static final String FOLDERS_SPLIT = "^**^"; 167 private static final Pattern FOLDERS_SPLIT_REGEX = Pattern.compile("\\^\\*\\*\\^"); 168 169 public Folder(Parcel in) { 170 id = in.readInt(); 171 uri = in.readParcelable(null); 172 name = in.readString(); 173 capabilities = in.readInt(); 174 // 1 for true, 0 for false. 175 hasChildren = in.readInt() == 1; 176 syncWindow = in.readInt(); 177 conversationListUri = in.readParcelable(null); 178 childFoldersListUri = in.readParcelable(null); 179 unreadCount = in.readInt(); 180 totalCount = in.readInt(); 181 refreshUri = in.readParcelable(null); 182 syncStatus = in.readInt(); 183 lastSyncResult = in.readInt(); 184 type = in.readInt(); 185 iconResId = in.readLong(); 186 bgColor = in.readString(); 187 fgColor = in.readString(); 188 loadMoreUri = in.readParcelable(null); 189 hierarchicalDesc = in.readString(); 190 parent = in.readParcelable(null); 191 } 192 193 public Folder(Cursor cursor) { 194 id = cursor.getInt(UIProvider.FOLDER_ID_COLUMN); 195 uri = Uri.parse(cursor.getString(UIProvider.FOLDER_URI_COLUMN)); 196 name = cursor.getString(UIProvider.FOLDER_NAME_COLUMN); 197 capabilities = cursor.getInt(UIProvider.FOLDER_CAPABILITIES_COLUMN); 198 // 1 for true, 0 for false. 199 hasChildren = cursor.getInt(UIProvider.FOLDER_HAS_CHILDREN_COLUMN) == 1; 200 syncWindow = cursor.getInt(UIProvider.FOLDER_SYNC_WINDOW_COLUMN); 201 String convList = cursor.getString(UIProvider.FOLDER_CONVERSATION_LIST_URI_COLUMN); 202 conversationListUri = !TextUtils.isEmpty(convList) ? Uri.parse(convList) : null; 203 String childList = cursor.getString(UIProvider.FOLDER_CHILD_FOLDERS_LIST_COLUMN); 204 childFoldersListUri = (hasChildren && !TextUtils.isEmpty(childList)) ? Uri.parse(childList) 205 : null; 206 unreadCount = cursor.getInt(UIProvider.FOLDER_UNREAD_COUNT_COLUMN); 207 totalCount = cursor.getInt(UIProvider.FOLDER_TOTAL_COUNT_COLUMN); 208 String refresh = cursor.getString(UIProvider.FOLDER_REFRESH_URI_COLUMN); 209 refreshUri = !TextUtils.isEmpty(refresh) ? Uri.parse(refresh) : null; 210 syncStatus = cursor.getInt(UIProvider.FOLDER_SYNC_STATUS_COLUMN); 211 lastSyncResult = cursor.getInt(UIProvider.FOLDER_LAST_SYNC_RESULT_COLUMN); 212 type = cursor.getInt(UIProvider.FOLDER_TYPE_COLUMN); 213 iconResId = cursor.getLong(UIProvider.FOLDER_ICON_RES_ID_COLUMN); 214 bgColor = cursor.getString(UIProvider.FOLDER_BG_COLOR_COLUMN); 215 fgColor = cursor.getString(UIProvider.FOLDER_FG_COLOR_COLUMN); 216 String loadMore = cursor.getString(UIProvider.FOLDER_LOAD_MORE_URI_COLUMN); 217 loadMoreUri = !TextUtils.isEmpty(loadMore) ? Uri.parse(loadMore) : null; 218 hierarchicalDesc = cursor.getString(UIProvider.FOLDER_HIERARCHICAL_DESC_COLUMN); 219 parent = null; 220 } 221 222 @Override 223 public void writeToParcel(Parcel dest, int flags) { 224 dest.writeInt(id); 225 dest.writeParcelable(uri, 0); 226 dest.writeString(name); 227 dest.writeInt(capabilities); 228 // 1 for true, 0 for false. 229 dest.writeInt(hasChildren ? 1 : 0); 230 dest.writeInt(syncWindow); 231 dest.writeParcelable(conversationListUri, 0); 232 dest.writeParcelable(childFoldersListUri, 0); 233 dest.writeInt(unreadCount); 234 dest.writeInt(totalCount); 235 dest.writeParcelable(refreshUri, 0); 236 dest.writeInt(syncStatus); 237 dest.writeInt(lastSyncResult); 238 dest.writeInt(type); 239 dest.writeLong(iconResId); 240 dest.writeString(bgColor); 241 dest.writeString(fgColor); 242 dest.writeParcelable(loadMoreUri, 0); 243 dest.writeString(hierarchicalDesc); 244 dest.writeParcelable(parent, 0); 245 } 246 247 /** 248 * Construct a folder that queries for search results. Do not call on the UI 249 * thread. 250 */ 251 public static CursorLoader forSearchResults(Account account, String query, Context context) { 252 if (account.searchUri != null) { 253 Builder searchBuilder = account.searchUri.buildUpon(); 254 searchBuilder.appendQueryParameter(UIProvider.SearchQueryParameters.QUERY, query); 255 Uri searchUri = searchBuilder.build(); 256 return new CursorLoader(context, searchUri, UIProvider.FOLDERS_PROJECTION, null, null, 257 null); 258 } 259 return null; 260 } 261 262 public static HashMap<Uri, Folder> hashMapForFolders(ArrayList<Folder> rawFolders) { 263 final HashMap<Uri, Folder> folders = new HashMap<Uri, Folder>(); 264 for (Folder f : rawFolders) { 265 folders.put(f.uri, f); 266 } 267 return folders; 268 } 269 270 private static Uri getValidUri(String uri) { 271 if (TextUtils.isEmpty(uri)) { 272 return null; 273 } 274 return Uri.parse(uri); 275 } 276 277 /** 278 * Constructor that leaves everything uninitialized. 279 */ 280 private Folder() { 281 name = FOLDER_UNINITIALIZED; 282 } 283 284 /** 285 * Creates a new instance of a folder object that is <b>not</b> initialized. The caller is 286 * expected to fill in the details. This resulting instance is not guaranteed to work 287 * correctly, and might break functionality. Use at your own risk!! 288 * @return a new instance of an unsafe folder. 289 */ 290 public static Folder newUnsafeInstance() { 291 return new Folder(); 292 } 293 294 @SuppressWarnings("hiding") 295 public static final Creator<Folder> CREATOR = new Creator<Folder>() { 296 @Override 297 public Folder createFromParcel(Parcel source) { 298 return new Folder(source); 299 } 300 301 @Override 302 public Folder[] newArray(int size) { 303 return new Folder[size]; 304 } 305 }; 306 307 @Override 308 public int describeContents() { 309 // Return a sort of version number for this parcelable folder. Starting with zero. 310 return 0; 311 } 312 313 @Override 314 public boolean equals(Object o) { 315 if (o == null || !(o instanceof Folder)) { 316 return false; 317 } 318 return Objects.equal(uri, ((Folder) o).uri); 319 } 320 321 @Override 322 public int hashCode() { 323 return uri == null ? 0 : uri.hashCode(); 324 } 325 326 @Override 327 public int compareTo(Folder other) { 328 return name.compareToIgnoreCase(other.name); 329 } 330 331 /** 332 * Create a Folder map from a string of serialized folders. This can only be done on the output 333 * of {@link #serialize(Map)}. 334 * @param serializedFolder A string obtained from {@link #serialize(Map)} 335 * @return a Map of folder name to folder. 336 */ 337 public static Map<String, Folder> parseFoldersFromString(String serializedFoldersString) { 338 if (TextUtils.isEmpty(serializedFoldersString)) { 339 return null; 340 } 341 Map<String, Folder> folderMap = Maps.newHashMap(); 342 ArrayList<Folder> folders = Folder.getFoldersArray(serializedFoldersString); 343 for (Folder folder : folders) { 344 if (folder.name != FOLDER_UNINITIALIZED) { 345 folderMap.put(folder.name, folder); 346 } 347 } 348 return folderMap; 349 } 350 351 /** 352 * Returns a boolean indicating whether network activity (sync) is occuring for this folder. 353 */ 354 public boolean isSyncInProgress() { 355 return UIProvider.SyncStatus.isSyncInProgress(syncStatus); 356 } 357 358 /** 359 * Serialize the given list of folders 360 * @param folderMap A valid map of folder names to Folders 361 * @return a string containing a serialized output of folder maps. 362 */ 363 public static String serialize(Map<String, Folder> folderMap) { 364 return getSerializedFolderString(folderMap.values()); 365 } 366 367 public boolean supportsCapability(int capability) { 368 return (capabilities & capability) != 0; 369 } 370 371 // Show black text on a transparent swatch for system folders, effectively hiding the 372 // swatch (see bug 2431925). 373 public static void setFolderBlockColor(Folder folder, View colorBlock) { 374 if (colorBlock == null) { 375 return; 376 } 377 boolean showBg = !TextUtils.isEmpty(folder.bgColor); 378 final int backgroundColor = showBg ? Integer.parseInt(folder.bgColor) : 0; 379 if (backgroundColor == Utils.getDefaultFolderBackgroundColor(colorBlock.getContext())) { 380 showBg = false; 381 } 382 if (!showBg) { 383 colorBlock.setBackgroundDrawable(null); 384 colorBlock.setVisibility(View.GONE); 385 } else { 386 PaintDrawable paintDrawable = new PaintDrawable(); 387 paintDrawable.getPaint().setColor(backgroundColor); 388 colorBlock.setBackgroundDrawable(paintDrawable); 389 colorBlock.setVisibility(View.VISIBLE); 390 } 391 } 392 393 public static void setIcon(Folder folder, ImageView iconView) { 394 if (iconView == null) { 395 return; 396 } 397 final long icon = folder.iconResId; 398 if (icon > 0) { 399 iconView.setImageResource((int)icon); 400 iconView.setVisibility(View.VISIBLE); 401 } else { 402 iconView.setVisibility(View.INVISIBLE); 403 } 404 } 405 406 /** 407 * Return if the type of the folder matches a provider defined folder. 408 */ 409 public boolean isProviderFolder() { 410 return type != UIProvider.FolderType.DEFAULT; 411 } 412 413 public int getBackgroundColor(int defaultColor) { 414 return TextUtils.isEmpty(bgColor) ? defaultColor : Integer.parseInt(bgColor); 415 } 416 417 public int getForegroundColor(int defaultColor) { 418 return TextUtils.isEmpty(fgColor) ? defaultColor : Integer.parseInt(fgColor); 419 } 420 421 public static String getSerializedFolderString(Collection<Folder> folders) { 422 final StringBuilder folderList = new StringBuilder(); 423 int i = 0; 424 for (Folder folderEntry : folders) { 425 folderList.append(Folder.toString(folderEntry)); 426 if (i < folders.size()) { 427 folderList.append(FOLDERS_SPLIT); 428 } 429 i++; 430 } 431 return folderList.toString(); 432 } 433 434 435 public static Folder fromString(String inString) { 436 if (TextUtils.isEmpty(inString)) { 437 return null; 438 } 439 final Folder f = new Folder(); 440 final String[] split = TextUtils.split(inString, SPLITTER_REGEX); 441 if (split.length < 20) { 442 return null; 443 } 444 f.id = Integer.parseInt(split[0]); 445 f.uri = Folder.getValidUri(split[1]); 446 f.name = split[2]; 447 f.hasChildren = Integer.parseInt(split[3]) != 0; 448 f.capabilities = Integer.parseInt(split[4]); 449 f.syncWindow = Integer.parseInt(split[5]); 450 f.conversationListUri = getValidUri(split[6]); 451 f.childFoldersListUri = getValidUri(split[7]); 452 f.unreadCount = Integer.parseInt(split[8]); 453 f.totalCount = Integer.parseInt(split[9]); 454 f.refreshUri = getValidUri(split[10]); 455 f.syncStatus = Integer.parseInt(split[11]); 456 f.lastSyncResult = Integer.parseInt(split[12]); 457 f.type = Integer.parseInt(split[13]); 458 f.iconResId = Integer.parseInt(split[14]); 459 f.bgColor = split[15]; 460 f.fgColor = split[16]; 461 f.loadMoreUri = getValidUri(split[17]); 462 f.hierarchicalDesc = split[18]; 463 f.parent = Folder.fromString(split[19]); 464 return f; 465 } 466 467 public static String toString(Folder folderEntry) { 468 StringBuilder builder = new StringBuilder(); 469 builder.append(folderEntry.id); 470 builder.append(SPLITTER); 471 builder.append(folderEntry.uri); 472 builder.append(SPLITTER); 473 builder.append(folderEntry.name); 474 builder.append(SPLITTER); 475 builder.append(folderEntry.hasChildren ? 1 : 0); 476 builder.append(SPLITTER); 477 builder.append(folderEntry.capabilities); 478 builder.append(SPLITTER); 479 builder.append(folderEntry.syncWindow); 480 builder.append(SPLITTER); 481 builder.append(folderEntry.conversationListUri); 482 builder.append(SPLITTER); 483 builder.append(folderEntry.childFoldersListUri); 484 builder.append(SPLITTER); 485 builder.append(folderEntry.unreadCount); 486 builder.append(SPLITTER); 487 builder.append(folderEntry.totalCount); 488 builder.append(SPLITTER); 489 builder.append(folderEntry.refreshUri); 490 builder.append(SPLITTER); 491 builder.append(folderEntry.syncStatus); 492 builder.append(SPLITTER); 493 builder.append(folderEntry.lastSyncResult); 494 builder.append(SPLITTER); 495 builder.append(folderEntry.type); 496 builder.append(SPLITTER); 497 builder.append(folderEntry.iconResId); 498 builder.append(SPLITTER); 499 builder.append(folderEntry.bgColor); 500 builder.append(SPLITTER); 501 builder.append(folderEntry.fgColor); 502 builder.append(SPLITTER); 503 builder.append(folderEntry.loadMoreUri); 504 builder.append(SPLITTER); 505 builder.append(folderEntry.hierarchicalDesc); 506 builder.append(SPLITTER); 507 if (folderEntry.parent != null) { 508 builder.append(Folder.toString(folderEntry.parent)); 509 } else { 510 builder.append(""); 511 } 512 return builder.toString(); 513 } 514 515 /** 516 * Returns a comma separated list of folder URIs for all the folders in the collection. 517 * @param folders 518 * @return 519 */ 520 public final static String getUriString(Collection<Folder> folders) { 521 final StringBuilder uris = new StringBuilder(); 522 boolean first = true; 523 for (Folder f : folders) { 524 if (first) { 525 first = false; 526 } else { 527 uris.append(','); 528 } 529 uris.append(f.uri.toString()); 530 } 531 return uris.toString(); 532 } 533 534 535 /** 536 * Get an array of folders from a rawFolders string. 537 */ 538 public static ArrayList<Folder> getFoldersArray(String rawFolders) { 539 if (TextUtils.isEmpty(rawFolders)) { 540 return null; 541 } 542 ArrayList<Folder> folders = new ArrayList<Folder>(); 543 String[] split = TextUtils.split(rawFolders, FOLDERS_SPLIT_REGEX); 544 Folder f; 545 for (String folder : split) { 546 f = Folder.fromString(folder); 547 if (f != null) { 548 folders.add(f); 549 } 550 } 551 return folders; 552 } 553 554 /** 555 * Get just the uri's from an arraylist of folders. 556 */ 557 public final static String[] getUriArray(ArrayList<Folder> folders) { 558 if (folders == null || folders.size() == 0) { 559 return new String[0]; 560 } 561 String[] folderUris = new String[folders.size()]; 562 int i = 0; 563 for (Folder folder : folders) { 564 folderUris[i] = folder.uri.toString(); 565 i++; 566 } 567 return folderUris; 568 } 569 570 /** 571 * Returns true if a conversation assigned to the needle will be assigned to the collection of 572 * folders in the haystack. False otherwise. This method is safe to call with null 573 * arguments. 574 * This method returns true under two circumstances 575 * <ul><li> If the URI of the needle was found in the collection of URIs that comprise the 576 * haystack. 577 * </li><li> If the needle is of the type Inbox, and at least one of the folders in the haystack 578 * are of type Inbox. <em>Rationale</em>: there are special folders that are marked as inbox, 579 * and the user might not have the control to assign conversations to them. This happens for 580 * the Priority Inbox in Gmail. When you assign a conversation to an Inbox folder, it will 581 * continue to appear in the Priority Inbox. However, the URI of Priority Inbox and Inbox will 582 * be different. So a direct equality check is insufficient. 583 * </li></ul> 584 * @param haystack a collection of folders, possibly overlapping 585 * @param needle a folder 586 * @return true if a conversation inside the needle will be in the folders in the haystack. 587 */ 588 public final static boolean containerIncludes(Collection<Folder> haystack, Folder needle) { 589 // If the haystack is empty, it cannot contain anything. 590 if (haystack == null || haystack.size() <= 0) { 591 return false; 592 } 593 // The null folder exists everywhere. 594 if (needle == null) { 595 return true; 596 } 597 boolean hasInbox = false; 598 // Get currently active folder info and compare it to the list 599 // these conversations have been given; if they no longer contain 600 // the selected folder, delete them from the list. 601 final Uri toFind = needle.uri; 602 for (Folder f : haystack) { 603 if (toFind.equals(f.uri)) { 604 return true; 605 } 606 hasInbox |= (f.type == UIProvider.FolderType.INBOX); 607 } 608 // Did not find the URI of needle directly. If the needle is an Inbox and one of the folders 609 // was an inbox, then the needle is contained (check Javadoc for explanation). 610 final boolean needleIsInbox = (needle.type == UIProvider.FolderType.INBOX); 611 return needleIsInbox ? hasInbox : false; 612 } 613 614 /** 615 * Returns a boolean indicating whether this Folder object has been initialized 616 */ 617 public boolean isInitialized() { 618 return name != FOLDER_UNINITIALIZED && conversationListUri != null && 619 !NULL_STRING_URI.equals(conversationListUri.toString()); 620 } 621 622 /** 623 * Returns a collection of a single folder. This method always returns a valid collection 624 * even if the input folder is null. 625 * @param in a folder, possibly null. 626 * @return a collection of the folder. 627 */ 628 public static Collection<Folder> listOf(Folder in) { 629 final Collection<Folder> target = (in == null) ? EMPTY : ImmutableList.of(in); 630 return target; 631 } 632 633 /** 634 * Return if this is the trash folder. 635 */ 636 public boolean isTrash() { 637 return type == UIProvider.FolderType.TRASH; 638 } 639 640 /** 641 * Return if this is a draft folder. 642 */ 643 public boolean isDraft() { 644 return type == UIProvider.FolderType.DRAFT; 645 } 646 647 /** 648 * Whether this folder supports only showing important messages. 649 */ 650 public boolean isImportantOnly() { 651 return supportsCapability( 652 UIProvider.FolderCapabilities.ONLY_IMPORTANT); 653 } 654 655 /** 656 * Whether this is the special folder just used to display all mail for an account. 657 */ 658 public boolean isViewAll() { 659 return type == UIProvider.FolderType.ALL_MAIL; 660 } 661 662 /** 663 * True if the previous sync was successful, false otherwise. 664 * @return 665 */ 666 public final boolean wasSyncSuccessful() { 667 return ((lastSyncResult & 0x0f) == UIProvider.LastSyncResult.SUCCESS); 668 } 669} 670