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