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