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