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