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