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