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