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