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