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