/******************************************************************************* * 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.Color; import android.graphics.drawable.PaintDrawable; import android.net.Uri; import android.net.Uri.Builder; import android.os.Parcel; import android.os.Parcelable; import android.os.Parcelable.Creator; import android.provider.BaseColumns; import android.text.TextUtils; import android.view.View; import com.android.mail.utils.LogUtils; import com.google.common.collect.Maps; import java.util.Collection; import java.util.Map; import java.util.regex.Pattern; /** * A folder is a collection of conversations, and perhaps other folders. */ public class Folder implements Parcelable { /** * */ private static final String FOLDER_UNINITIALIZED = "Uninitialized!"; // Try to match the order of members with the order of constants in UIProvider. /** * Unique id of this folder. */ public int id; /** * 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 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; /** * The result of the last sync for this folder */ public int lastSyncResult; /** * Folder type. 0 is default. */ public int type; /** * Icon for this folder; 0 implies no icon. */ public long iconResId; public String bgColor; public String fgColor; /** * The content provider URI to request additional conversations */ public Uri loadMoreUri; /** * Total number of members that comprise an instance of a folder. This is * the number of members that need to be serialized or parceled. */ private static final int NUMBER_MEMBERS = UIProvider.FOLDERS_PROJECTION.length; /** * Used only for debugging. */ private static final String LOG_TAG = new LogUtils().getLogTag(); /** * Examples of expected format for the joined folder strings * * Example of a joined folder string: * 630107622^*^^i^*^^i^*^0 * ^*^^*^^*^ * * The sqlite queries will return a list of folder strings separated with "^**^" * Example of a query result: * 630107622^*^^i^*^^i^*^0^**^630107626^*^^u^*^^u^*^0^**^630107627^*^^f^*^^f^*^0 */ private static final String FOLDER_COMPONENT_SEPARATOR = "^*^"; private static final Pattern FOLDER_COMPONENT_SEPARATOR_PATTERN = Pattern.compile("\\^\\*\\^"); public static final String FOLDER_SEPARATOR = "^**^"; public static final Pattern FOLDER_SEPARATOR_PATTERN = Pattern.compile("\\^\\*\\*\\^"); public Folder(Parcel in) { assert (in.dataSize() == NUMBER_MEMBERS); id = in.readInt(); uri = in.readParcelable(null); name = in.readString(); capabilities = in.readInt(); // 1 for true, 0 for false. hasChildren = in.readInt() == 1; syncWindow = in.readInt(); conversationListUri = in.readParcelable(null); childFoldersListUri = in.readParcelable(null); unreadCount = in.readInt(); totalCount = in.readInt(); refreshUri = in.readParcelable(null); syncStatus = in.readInt(); lastSyncResult = in.readInt(); type = in.readInt(); iconResId = in.readLong(); bgColor = in.readString(); fgColor = in.readString(); loadMoreUri = in.readParcelable(null); } public Folder(Cursor cursor) { assert (cursor.getColumnCount() == NUMBER_MEMBERS); id = cursor.getInt(UIProvider.FOLDER_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; 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.getLong(UIProvider.FOLDER_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; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(id); 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(unreadCount); dest.writeInt(totalCount); dest.writeParcelable(refreshUri, 0); dest.writeInt(syncStatus); dest.writeInt(lastSyncResult); dest.writeInt(type); dest.writeLong(iconResId); dest.writeString(bgColor); dest.writeString(fgColor); dest.writeParcelable(loadMoreUri, 0); } /** * Return a serialized String for this folder. */ public synchronized String serialize() { StringBuilder out = new StringBuilder(); out.append(id).append(FOLDER_COMPONENT_SEPARATOR); out.append(uri).append(FOLDER_COMPONENT_SEPARATOR); out.append(name).append(FOLDER_COMPONENT_SEPARATOR); out.append(capabilities).append(FOLDER_COMPONENT_SEPARATOR); out.append(hasChildren ? "1": "0").append(FOLDER_COMPONENT_SEPARATOR); out.append(syncWindow).append(FOLDER_COMPONENT_SEPARATOR); out.append(conversationListUri).append(FOLDER_COMPONENT_SEPARATOR); out.append(childFoldersListUri).append(FOLDER_COMPONENT_SEPARATOR); out.append(unreadCount).append(FOLDER_COMPONENT_SEPARATOR); out.append(totalCount).append(FOLDER_COMPONENT_SEPARATOR); out.append(refreshUri).append(FOLDER_COMPONENT_SEPARATOR); out.append(syncStatus).append(FOLDER_COMPONENT_SEPARATOR); out.append(lastSyncResult).append(FOLDER_COMPONENT_SEPARATOR); out.append(type).append(FOLDER_COMPONENT_SEPARATOR); out.append(iconResId).append(FOLDER_COMPONENT_SEPARATOR); out.append(bgColor).append(FOLDER_COMPONENT_SEPARATOR); out.append(fgColor).append(FOLDER_COMPONENT_SEPARATOR); out.append(loadMoreUri); return out.toString(); } /** * Construct a folder that queries for search results. Do not call on the UI * thread. */ public static Folder forSearchResults(Account account, String query, Context context) { Folder searchFolder = null; if (account.searchUri != null) { Builder searchBuilder = account.searchUri.buildUpon(); searchBuilder.appendQueryParameter(UIProvider.SearchQueryParameters.QUERY, query); Uri searchUri = searchBuilder.build(); Cursor folderCursor = context.getContentResolver().query(searchUri, UIProvider.FOLDERS_PROJECTION, null, null, null); if (folderCursor != null) { folderCursor.moveToFirst(); searchFolder = new Folder(folderCursor); } } return searchFolder; } /** * Construct a new Folder instance from a previously serialized string. * @param serializedFolder string obtained from {@link #serialize()} on a valid folder. */ public Folder(String serializedFolder) { String[] folderMembers = TextUtils.split(serializedFolder, FOLDER_COMPONENT_SEPARATOR_PATTERN); if (folderMembers.length != NUMBER_MEMBERS) { throw new IllegalArgumentException( "Folder de-serializing failed. Wrong number of members detected." + folderMembers.length); } id = Integer.valueOf(folderMembers[0]); uri = Uri.parse(folderMembers[1]); name = folderMembers[2]; capabilities = Integer.valueOf(folderMembers[3]); // 1 for true, 0 for false hasChildren = folderMembers[4] == "1"; syncWindow = Integer.valueOf(folderMembers[5]); String convList = folderMembers[6]; conversationListUri = !TextUtils.isEmpty(convList) ? Uri.parse(convList) : null; String childList = folderMembers[7]; childFoldersListUri = (hasChildren && !TextUtils.isEmpty(childList)) ? Uri.parse(childList) : null; unreadCount = Integer.valueOf(folderMembers[8]); totalCount = Integer.valueOf(folderMembers[9]); String refresh = folderMembers[10]; refreshUri = !TextUtils.isEmpty(refresh) ? Uri.parse(refresh) : null; syncStatus = Integer.valueOf(folderMembers[11]); lastSyncResult = Integer.valueOf(folderMembers[12]); type = Integer.valueOf(folderMembers[13]); iconResId = Long.valueOf(folderMembers[14]); bgColor = folderMembers[15]; fgColor = folderMembers[16]; String loadMore = folderMembers[17]; loadMoreUri = !TextUtils.isEmpty(loadMore) ? Uri.parse(loadMore) : null; } /** * Constructor that leaves everything uninitialized. For use only by {@link #serialize()} * which is responsible for filling in all the fields */ public Folder() { name = FOLDER_UNINITIALIZED; } @SuppressWarnings("hiding") public static final Creator CREATOR = new Creator() { @Override public Folder createFromParcel(Parcel source) { return new Folder(source); } @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; } /** * Create a Folder map from a string of serialized folders. This can only be done on the output * of {@link #serialize(Map)}. * @param serializedFolder A string obtained from {@link #serialize(Map)} * @return a Map of folder name to folder. */ public static Map parseFoldersFromString(String serializedFolder) { LogUtils.d(LOG_TAG, "folder query result: %s", serializedFolder); Map folderMap = Maps.newHashMap(); if (serializedFolder == null || serializedFolder == "") { return folderMap; } String[] folderPieces = TextUtils.split( serializedFolder, FOLDER_COMPONENT_SEPARATOR_PATTERN); for (int i = 0, n = folderPieces.length; i < n; i++) { Folder folder = new Folder(folderPieces[i]); if (folder.name != FOLDER_UNINITIALIZED) { folderMap.put(folder.name, folder); } } return folderMap; } /** * Returns a boolean indicating whether network activity (sync) is occuring for this folder. */ public boolean isSyncInProgress() { return 0 != (syncStatus & (UIProvider.SyncStatus.BACKGROUND_SYNC | UIProvider.SyncStatus.USER_REFRESH | UIProvider.SyncStatus.USER_QUERY | UIProvider.SyncStatus.USER_MORE_RESULTS)); } /** * Serialize the given list of folders * @param folderMap A valid map of folder names to Folders * @return a string containing a serialized output of folder maps. */ public static String serialize(Map folderMap) { Collection folderCollection = folderMap.values(); Folder[] folderList = folderCollection.toArray(new Folder[]{} ); int numFolders = folderList.length; StringBuilder result = new StringBuilder(); for (int i = 0; i < numFolders; i++) { if (i > 0) { result.append(FOLDER_SEPARATOR); } Folder folder = folderList[i]; result.append(folder.serialize()); } return result.toString(); } 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) { final boolean showBg = !TextUtils.isEmpty(folder.bgColor); final int backgroundColor = showBg ? Color.parseColor(folder.bgColor) : 0; if (!showBg) { colorBlock.setBackgroundDrawable(null); } else { PaintDrawable paintDrawable = new PaintDrawable(); paintDrawable.getPaint().setColor(backgroundColor); colorBlock.setBackgroundDrawable(paintDrawable); } } /** * Return if the type of the folder matches a provider defined folder. */ public static boolean isProviderFolder(Folder folder) { int type = folder.type; return type == UIProvider.FolderType.DEFAULT || type == UIProvider.FolderType.INBOX || type == UIProvider.FolderType.DRAFT || type == UIProvider.FolderType.OUTBOX || type == UIProvider.FolderType.SENT || type == UIProvider.FolderType.TRASH || type == UIProvider.FolderType.SPAM; } }