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