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