| /******************************************************************************* |
| * Copyright (C) 2012 Google Inc. |
| * Licensed to The Android Open Source Project. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| *******************************************************************************/ |
| |
| package com.android.mail.providers; |
| |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.graphics.drawable.PaintDrawable; |
| import android.net.Uri; |
| import android.net.Uri.Builder; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.text.TextUtils; |
| import android.view.View; |
| import android.widget.ImageView; |
| |
| import com.android.mail.content.CursorCreator; |
| import com.android.mail.content.ObjectCursorLoader; |
| import com.android.mail.providers.UIProvider.FolderType; |
| import com.android.mail.utils.LogTag; |
| import com.android.mail.utils.LogUtils; |
| import com.android.mail.utils.Utils; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Objects; |
| import com.google.common.collect.ImmutableList; |
| |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| |
| /** |
| * A folder is a collection of conversations, and perhaps other folders. |
| */ |
| // TODO: make most of these fields final |
| public class Folder implements Parcelable, Comparable<Folder> { |
| /** |
| * |
| */ |
| private static final String FOLDER_UNINITIALIZED = "Uninitialized!"; |
| |
| // TODO: remove this once we figure out which folder is returning a "null" string as the |
| // conversation list uri |
| private static final String NULL_STRING_URI = "null"; |
| private static final String LOG_TAG = LogTag.getLogTag(); |
| |
| // Try to match the order of members with the order of constants in UIProvider. |
| |
| /** |
| * Unique id of this folder. |
| */ |
| public int id; |
| |
| /** |
| * Persistent (across installations) id of this folder. |
| */ |
| public String persistentId; |
| |
| /** |
| * The content provider URI that returns this folder for this account. |
| */ |
| public Uri uri; |
| |
| /** |
| * The human visible name for this folder. |
| */ |
| public String name; |
| |
| /** |
| * The possible capabilities that this folder supports. |
| */ |
| public int capabilities; |
| |
| /** |
| * Whether or not this folder has children folders. |
| */ |
| public boolean hasChildren; |
| |
| /** |
| * How large the synchronization window is: how many days worth of data is retained on the |
| * device. |
| */ |
| public int syncWindow; |
| |
| /** |
| * The content provider URI to return the list of conversations in this |
| * folder. |
| */ |
| public Uri conversationListUri; |
| |
| /** |
| * The content provider URI to return the list of child folders of this folder. |
| */ |
| public Uri childFoldersListUri; |
| |
| /** |
| * The number of messages that are unseen in this folder. |
| */ |
| public int unseenCount; |
| |
| /** |
| * The number of messages that are unread in this folder. |
| */ |
| public int unreadCount; |
| |
| /** |
| * The total number of messages in this folder. |
| */ |
| public int totalCount; |
| |
| /** |
| * The content provider URI to force a refresh of this folder. |
| */ |
| public Uri refreshUri; |
| |
| /** |
| * The current sync status of the folder |
| */ |
| public int syncStatus; |
| |
| /** |
| * A packed integer containing the last synced result, and the request code. The |
| * value is (requestCode << 4) | syncResult |
| * syncResult is a value from {@link UIProvider.LastSyncResult} |
| * requestCode is a value from: {@link UIProvider.SyncStatus}, |
| */ |
| public int lastSyncResult; |
| |
| /** |
| * Folder type bit mask. 0 is default. |
| * @see FolderType |
| */ |
| public int type; |
| |
| /** |
| * Icon for this folder; 0 implies no icon. |
| */ |
| public int iconResId; |
| |
| /** |
| * Notification icon for this folder; 0 implies no icon. |
| */ |
| public int notificationIconResId; |
| |
| public String bgColor; |
| public String fgColor; |
| |
| /** |
| * The content provider URI to request additional conversations |
| */ |
| public Uri loadMoreUri; |
| |
| /** |
| * The possibly empty name of this folder with full hierarchy. |
| * The expected format is: parent/folder1/folder2/folder3/folder4 |
| */ |
| public String hierarchicalDesc; |
| |
| /** |
| * Parent folder of this folder, or null if there is none. This is set as |
| * part of the execution of the application and not obtained or stored via |
| * the provider. |
| */ |
| public Folder parent; |
| |
| /** |
| * The time at which the last message was received. |
| */ |
| public long lastMessageTimestamp; |
| |
| /** An immutable, empty conversation list */ |
| public static final Collection<Folder> EMPTY = Collections.emptyList(); |
| |
| // TODO: we desperately need a Builder here |
| public Folder(int id, String persistentId, Uri uri, String name, int capabilities, |
| boolean hasChildren, int syncWindow, Uri conversationListUri, Uri childFoldersListUri, |
| int unseenCount, int unreadCount, int totalCount, Uri refreshUri, int syncStatus, |
| int lastSyncResult, int type, int iconResId, int notificationIconResId, String bgColor, |
| String fgColor, Uri loadMoreUri, String hierarchicalDesc, Folder parent, |
| final long lastMessageTimestamp) { |
| this.id = id; |
| this.persistentId = persistentId; |
| this.uri = uri; |
| this.name = name; |
| this.capabilities = capabilities; |
| this.hasChildren = hasChildren; |
| this.syncWindow = syncWindow; |
| this.conversationListUri = conversationListUri; |
| this.childFoldersListUri = childFoldersListUri; |
| this.unseenCount = unseenCount; |
| this.unreadCount = unreadCount; |
| this.totalCount = totalCount; |
| this.refreshUri = refreshUri; |
| this.syncStatus = syncStatus; |
| this.lastSyncResult = lastSyncResult; |
| this.type = type; |
| this.iconResId = iconResId; |
| this.notificationIconResId = notificationIconResId; |
| this.bgColor = bgColor; |
| this.fgColor = fgColor; |
| this.loadMoreUri = loadMoreUri; |
| this.hierarchicalDesc = hierarchicalDesc; |
| this.parent = parent; |
| this.lastMessageTimestamp = lastMessageTimestamp; |
| } |
| |
| public Folder(Cursor cursor) { |
| id = cursor.getInt(UIProvider.FOLDER_ID_COLUMN); |
| persistentId = cursor.getString(UIProvider.FOLDER_PERSISTENT_ID_COLUMN); |
| uri = Uri.parse(cursor.getString(UIProvider.FOLDER_URI_COLUMN)); |
| name = cursor.getString(UIProvider.FOLDER_NAME_COLUMN); |
| capabilities = cursor.getInt(UIProvider.FOLDER_CAPABILITIES_COLUMN); |
| // 1 for true, 0 for false. |
| hasChildren = cursor.getInt(UIProvider.FOLDER_HAS_CHILDREN_COLUMN) == 1; |
| syncWindow = cursor.getInt(UIProvider.FOLDER_SYNC_WINDOW_COLUMN); |
| String convList = cursor.getString(UIProvider.FOLDER_CONVERSATION_LIST_URI_COLUMN); |
| conversationListUri = !TextUtils.isEmpty(convList) ? Uri.parse(convList) : null; |
| String childList = cursor.getString(UIProvider.FOLDER_CHILD_FOLDERS_LIST_COLUMN); |
| childFoldersListUri = (hasChildren && !TextUtils.isEmpty(childList)) ? Uri.parse(childList) |
| : null; |
| unseenCount = cursor.getInt(UIProvider.FOLDER_UNSEEN_COUNT_COLUMN); |
| unreadCount = cursor.getInt(UIProvider.FOLDER_UNREAD_COUNT_COLUMN); |
| totalCount = cursor.getInt(UIProvider.FOLDER_TOTAL_COUNT_COLUMN); |
| String refresh = cursor.getString(UIProvider.FOLDER_REFRESH_URI_COLUMN); |
| refreshUri = !TextUtils.isEmpty(refresh) ? Uri.parse(refresh) : null; |
| syncStatus = cursor.getInt(UIProvider.FOLDER_SYNC_STATUS_COLUMN); |
| lastSyncResult = cursor.getInt(UIProvider.FOLDER_LAST_SYNC_RESULT_COLUMN); |
| type = cursor.getInt(UIProvider.FOLDER_TYPE_COLUMN); |
| iconResId = cursor.getInt(UIProvider.FOLDER_ICON_RES_ID_COLUMN); |
| notificationIconResId = cursor.getInt(UIProvider.FOLDER_NOTIFICATION_ICON_RES_ID_COLUMN); |
| bgColor = cursor.getString(UIProvider.FOLDER_BG_COLOR_COLUMN); |
| fgColor = cursor.getString(UIProvider.FOLDER_FG_COLOR_COLUMN); |
| String loadMore = cursor.getString(UIProvider.FOLDER_LOAD_MORE_URI_COLUMN); |
| loadMoreUri = !TextUtils.isEmpty(loadMore) ? Uri.parse(loadMore) : null; |
| hierarchicalDesc = cursor.getString(UIProvider.FOLDER_HIERARCHICAL_DESC_COLUMN); |
| parent = null; |
| lastMessageTimestamp = cursor.getLong(UIProvider.FOLDER_LAST_MESSAGE_TIMESTAMP_COLUMN); |
| } |
| |
| /** |
| * Public object that knows how to construct Folders given Cursors. |
| */ |
| public static final CursorCreator<Folder> FACTORY = new CursorCreator<Folder>() { |
| @Override |
| public Folder createFromCursor(Cursor c) { |
| return new Folder(c); |
| } |
| |
| @Override |
| public String toString() { |
| return "Folder CursorCreator"; |
| } |
| }; |
| |
| public Folder(Parcel in, ClassLoader loader) { |
| id = in.readInt(); |
| persistentId = in.readString(); |
| uri = in.readParcelable(loader); |
| name = in.readString(); |
| capabilities = in.readInt(); |
| // 1 for true, 0 for false. |
| hasChildren = in.readInt() == 1; |
| syncWindow = in.readInt(); |
| conversationListUri = in.readParcelable(loader); |
| childFoldersListUri = in.readParcelable(loader); |
| unseenCount = in.readInt(); |
| unreadCount = in.readInt(); |
| totalCount = in.readInt(); |
| refreshUri = in.readParcelable(loader); |
| syncStatus = in.readInt(); |
| lastSyncResult = in.readInt(); |
| type = in.readInt(); |
| iconResId = in.readInt(); |
| notificationIconResId = in.readInt(); |
| bgColor = in.readString(); |
| fgColor = in.readString(); |
| loadMoreUri = in.readParcelable(loader); |
| hierarchicalDesc = in.readString(); |
| parent = in.readParcelable(loader); |
| lastMessageTimestamp = in.readLong(); |
| } |
| |
| @Override |
| public void writeToParcel(Parcel dest, int flags) { |
| dest.writeInt(id); |
| dest.writeString(persistentId); |
| dest.writeParcelable(uri, 0); |
| dest.writeString(name); |
| dest.writeInt(capabilities); |
| // 1 for true, 0 for false. |
| dest.writeInt(hasChildren ? 1 : 0); |
| dest.writeInt(syncWindow); |
| dest.writeParcelable(conversationListUri, 0); |
| dest.writeParcelable(childFoldersListUri, 0); |
| dest.writeInt(unseenCount); |
| dest.writeInt(unreadCount); |
| dest.writeInt(totalCount); |
| dest.writeParcelable(refreshUri, 0); |
| dest.writeInt(syncStatus); |
| dest.writeInt(lastSyncResult); |
| dest.writeInt(type); |
| dest.writeInt(iconResId); |
| dest.writeInt(notificationIconResId); |
| dest.writeString(bgColor); |
| dest.writeString(fgColor); |
| dest.writeParcelable(loadMoreUri, 0); |
| dest.writeString(hierarchicalDesc); |
| dest.writeParcelable(parent, 0); |
| dest.writeLong(lastMessageTimestamp); |
| } |
| |
| /** |
| * Construct a folder that queries for search results. Do not call on the UI |
| * thread. |
| */ |
| public static ObjectCursorLoader<Folder> forSearchResults(Account account, String query, |
| Context context) { |
| if (account.searchUri != null) { |
| final Builder searchBuilder = account.searchUri.buildUpon(); |
| searchBuilder.appendQueryParameter(UIProvider.SearchQueryParameters.QUERY, query); |
| final Uri searchUri = searchBuilder.build(); |
| return new ObjectCursorLoader<Folder>(context, searchUri, UIProvider.FOLDERS_PROJECTION, |
| FACTORY); |
| } |
| return null; |
| } |
| |
| public static HashMap<Uri, Folder> hashMapForFolders(List<Folder> rawFolders) { |
| final HashMap<Uri, Folder> folders = new HashMap<Uri, Folder>(); |
| for (Folder f : rawFolders) { |
| folders.put(f.uri, f); |
| } |
| return folders; |
| } |
| |
| /** |
| * Constructor that leaves everything uninitialized. |
| */ |
| private Folder() { |
| name = FOLDER_UNINITIALIZED; |
| } |
| |
| /** |
| * Creates a new instance of a folder object that is <b>not</b> initialized. The caller is |
| * expected to fill in the details. Used only for testing. |
| * @return a new instance of an unsafe folder. |
| */ |
| @VisibleForTesting |
| public static Folder newUnsafeInstance() { |
| return new Folder(); |
| } |
| |
| public static final ClassLoaderCreator<Folder> CREATOR = new ClassLoaderCreator<Folder>() { |
| @Override |
| public Folder createFromParcel(Parcel source) { |
| return new Folder(source, null); |
| } |
| |
| @Override |
| public Folder createFromParcel(Parcel source, ClassLoader loader) { |
| return new Folder(source, loader); |
| } |
| |
| @Override |
| public Folder[] newArray(int size) { |
| return new Folder[size]; |
| } |
| }; |
| |
| @Override |
| public int describeContents() { |
| // Return a sort of version number for this parcelable folder. Starting with zero. |
| return 0; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (o == null || !(o instanceof Folder)) { |
| return false; |
| } |
| return Objects.equal(uri, ((Folder) o).uri); |
| } |
| |
| @Override |
| public int hashCode() { |
| return uri == null ? 0 : uri.hashCode(); |
| } |
| |
| @Override |
| public String toString() { |
| // log extra info at DEBUG level or finer |
| final StringBuilder sb = new StringBuilder("[folder id="); |
| sb.append(id); |
| if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) { |
| sb.append(", uri="); |
| sb.append(uri); |
| sb.append(", name="); |
| sb.append(name); |
| } |
| sb.append("]"); |
| return sb.toString(); |
| } |
| |
| @Override |
| public int compareTo(Folder other) { |
| return name.compareToIgnoreCase(other.name); |
| } |
| |
| /** |
| * Returns a boolean indicating whether network activity (sync) is occuring for this folder. |
| */ |
| public boolean isSyncInProgress() { |
| return UIProvider.SyncStatus.isSyncInProgress(syncStatus); |
| } |
| |
| public boolean supportsCapability(int capability) { |
| return (capabilities & capability) != 0; |
| } |
| |
| // Show black text on a transparent swatch for system folders, effectively hiding the |
| // swatch (see bug 2431925). |
| public static void setFolderBlockColor(Folder folder, View colorBlock) { |
| if (colorBlock == null) { |
| return; |
| } |
| boolean showBg = |
| !TextUtils.isEmpty(folder.bgColor) && (folder.type & FolderType.INBOX_SECTION) == 0; |
| final int backgroundColor = showBg ? Integer.parseInt(folder.bgColor) : 0; |
| if (backgroundColor == Utils.getDefaultFolderBackgroundColor(colorBlock.getContext())) { |
| showBg = false; |
| } |
| if (!showBg) { |
| colorBlock.setBackgroundDrawable(null); |
| colorBlock.setVisibility(View.GONE); |
| } else { |
| PaintDrawable paintDrawable = new PaintDrawable(); |
| paintDrawable.getPaint().setColor(backgroundColor); |
| colorBlock.setBackgroundDrawable(paintDrawable); |
| colorBlock.setVisibility(View.VISIBLE); |
| } |
| } |
| |
| public static void setIcon(Folder folder, ImageView iconView) { |
| if (iconView == null) { |
| return; |
| } |
| final int icon = folder.iconResId; |
| if (icon > 0) { |
| iconView.setImageResource(icon); |
| iconView.setVisibility(View.VISIBLE); |
| } else { |
| iconView.setVisibility(View.GONE); |
| } |
| } |
| |
| /** |
| * Return if the type of the folder matches a provider defined folder. |
| */ |
| public boolean isProviderFolder() { |
| return !isType(UIProvider.FolderType.DEFAULT); |
| } |
| |
| public int getBackgroundColor(int defaultColor) { |
| return getNonEmptyColor(bgColor, defaultColor); |
| } |
| |
| public int getForegroundColor(int defaultColor) { |
| return getNonEmptyColor(fgColor, defaultColor); |
| } |
| |
| /** |
| * Returns the candidate color if non-emptyp, or the default if the candidate is empty |
| * @param candidate |
| * @return |
| */ |
| public static int getNonEmptyColor(String candidate, int defaultColor) { |
| return TextUtils.isEmpty(candidate) ? defaultColor : Integer.parseInt(candidate); |
| |
| } |
| |
| /** |
| * Returns a comma separated list of folder URIs for all the folders in the collection. |
| * @param folders |
| * @return |
| */ |
| public final static String getUriString(Collection<Folder> folders) { |
| final StringBuilder uris = new StringBuilder(); |
| boolean first = true; |
| for (Folder f : folders) { |
| if (first) { |
| first = false; |
| } else { |
| uris.append(','); |
| } |
| uris.append(f.uri.toString()); |
| } |
| return uris.toString(); |
| } |
| |
| /** |
| * Get just the uri's from an arraylist of folders. |
| */ |
| public final static String[] getUriArray(List<Folder> folders) { |
| if (folders == null || folders.size() == 0) { |
| return new String[0]; |
| } |
| String[] folderUris = new String[folders.size()]; |
| int i = 0; |
| for (Folder folder : folders) { |
| folderUris[i] = folder.uri.toString(); |
| i++; |
| } |
| return folderUris; |
| } |
| |
| /** |
| * Returns true if a conversation assigned to the needle will be assigned to the collection of |
| * folders in the haystack. False otherwise. This method is safe to call with null |
| * arguments. |
| * This method returns true under two circumstances |
| * <ul><li> If the URI of the needle was found in the collection of URIs that comprise the |
| * haystack. |
| * </li><li> If the needle is of the type Inbox, and at least one of the folders in the haystack |
| * are of type Inbox. <em>Rationale</em>: there are special folders that are marked as inbox, |
| * and the user might not have the control to assign conversations to them. This happens for |
| * the Priority Inbox in Gmail. When you assign a conversation to an Inbox folder, it will |
| * continue to appear in the Priority Inbox. However, the URI of Priority Inbox and Inbox will |
| * be different. So a direct equality check is insufficient. |
| * </li></ul> |
| * @param haystack a collection of folders, possibly overlapping |
| * @param needle a folder |
| * @return true if a conversation inside the needle will be in the folders in the haystack. |
| */ |
| public final static boolean containerIncludes(Collection<Folder> haystack, Folder needle) { |
| // If the haystack is empty, it cannot contain anything. |
| if (haystack == null || haystack.size() <= 0) { |
| return false; |
| } |
| // The null folder exists everywhere. |
| if (needle == null) { |
| return true; |
| } |
| boolean hasInbox = false; |
| // Get currently active folder info and compare it to the list |
| // these conversations have been given; if they no longer contain |
| // the selected folder, delete them from the list. |
| final Uri toFind = needle.uri; |
| for (Folder f : haystack) { |
| if (toFind.equals(f.uri)) { |
| return true; |
| } |
| hasInbox |= f.isInbox(); |
| } |
| // Did not find the URI of needle directly. If the needle is an Inbox and one of the folders |
| // was an inbox, then the needle is contained (check Javadoc for explanation). |
| final boolean needleIsInbox = needle.isInbox(); |
| return needleIsInbox ? hasInbox : false; |
| } |
| |
| /** |
| * Returns a boolean indicating whether this Folder object has been initialized |
| */ |
| public boolean isInitialized() { |
| return name != FOLDER_UNINITIALIZED && conversationListUri != null && |
| !NULL_STRING_URI.equals(conversationListUri.toString()); |
| } |
| |
| /** |
| * Returns a collection of a single folder. This method always returns a valid collection |
| * even if the input folder is null. |
| * @param in a folder, possibly null. |
| * @return a collection of the folder. |
| */ |
| public static Collection<Folder> listOf(Folder in) { |
| final Collection<Folder> target = (in == null) ? EMPTY : ImmutableList.of(in); |
| return target; |
| } |
| |
| public boolean isType(final int folderType) { |
| return (type & folderType) != 0; |
| } |
| |
| public boolean isInbox() { |
| return isType(UIProvider.FolderType.INBOX); |
| } |
| |
| /** |
| * Return if this is the trash folder. |
| */ |
| public boolean isTrash() { |
| return isType(UIProvider.FolderType.TRASH); |
| } |
| |
| /** |
| * Return if this is a draft folder. |
| */ |
| public boolean isDraft() { |
| return isType(UIProvider.FolderType.DRAFT); |
| } |
| |
| /** |
| * Whether this folder supports only showing important messages. |
| */ |
| public boolean isImportantOnly() { |
| return supportsCapability( |
| UIProvider.FolderCapabilities.ONLY_IMPORTANT); |
| } |
| |
| /** |
| * Whether this is the special folder just used to display all mail for an account. |
| */ |
| public boolean isViewAll() { |
| return isType(UIProvider.FolderType.ALL_MAIL); |
| } |
| |
| /** |
| * True if the previous sync was successful, false otherwise. |
| * @return |
| */ |
| public final boolean wasSyncSuccessful() { |
| return ((lastSyncResult & 0x0f) == UIProvider.LastSyncResult.SUCCESS); |
| } |
| |
| /** |
| * Don't use this for ANYTHING but the FolderListAdapter. It does not have |
| * all the fields. |
| */ |
| public static Folder getDeficientDisplayOnlyFolder(Cursor cursor) { |
| Folder f = new Folder(); |
| f.id = cursor.getInt(UIProvider.FOLDER_ID_COLUMN); |
| f.uri = Utils.getValidUri(cursor.getString(UIProvider.FOLDER_URI_COLUMN)); |
| f.totalCount = cursor.getInt(UIProvider.FOLDER_TOTAL_COUNT_COLUMN); |
| f.unseenCount = cursor.getInt(UIProvider.FOLDER_UNSEEN_COUNT_COLUMN); |
| f.unreadCount = cursor.getInt(UIProvider.FOLDER_UNREAD_COUNT_COLUMN); |
| f.conversationListUri = Utils.getValidUri(cursor |
| .getString(UIProvider.FOLDER_CONVERSATION_LIST_URI_COLUMN)); |
| f.type = cursor.getInt(UIProvider.FOLDER_TYPE_COLUMN); |
| f.capabilities = cursor.getInt(UIProvider.FOLDER_CAPABILITIES_COLUMN); |
| f.bgColor = cursor.getString(UIProvider.FOLDER_BG_COLOR_COLUMN); |
| f.name = cursor.getString(UIProvider.FOLDER_NAME_COLUMN); |
| f.iconResId = cursor.getInt(UIProvider.FOLDER_ICON_RES_ID_COLUMN); |
| f.notificationIconResId = cursor.getInt(UIProvider.FOLDER_NOTIFICATION_ICON_RES_ID_COLUMN); |
| f.lastMessageTimestamp = cursor.getLong(UIProvider.FOLDER_LAST_MESSAGE_TIMESTAMP_COLUMN); |
| return f; |
| } |
| } |