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