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