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