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