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