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