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