Folder.java revision 7b6d03db55338cbf9717896f99eb20d02bf371e4
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.google.common.base.Objects;
33import com.google.common.collect.ImmutableList;
34import com.google.common.collect.Maps;
35
36import java.util.ArrayList;
37import java.util.Collection;
38import java.util.Collections;
39import java.util.HashMap;
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     * The possibly empty name of this folder with full hierarchy.
141     * The expected format is: parent/folder1/folder2/folder3/folder4
142     */
143    public String hierarchicalDesc;
144
145    /**
146     * Parent folder of this folder, or null if there is none. This is set as
147     * part of the execution of the application and not obtained or stored via
148     * the provider.
149     */
150    public Folder parent;
151
152    /** An immutable, empty conversation list */
153    public static final Collection<Folder> EMPTY = Collections.emptyList();
154
155    public static String SPLITTER = "^*^";
156    private static Pattern SPLITTER_REGEX = Pattern.compile("\\^\\*\\^");
157    public static final String FOLDERS_SPLIT = "^**^";
158    private static Pattern FOLDERS_SPLIT_REGEX = Pattern.compile("\\^\\*\\*\\^");
159
160    public Folder(Parcel in) {
161        id = in.readInt();
162        uri = in.readParcelable(null);
163        name = in.readString();
164        capabilities = in.readInt();
165        // 1 for true, 0 for false.
166        hasChildren = in.readInt() == 1;
167        syncWindow = in.readInt();
168        conversationListUri = in.readParcelable(null);
169        childFoldersListUri = in.readParcelable(null);
170        unreadCount = in.readInt();
171        totalCount = in.readInt();
172        refreshUri = in.readParcelable(null);
173        syncStatus = in.readInt();
174        lastSyncResult = in.readInt();
175        type = in.readInt();
176        iconResId = in.readLong();
177        bgColor = in.readString();
178        fgColor = in.readString();
179        loadMoreUri = in.readParcelable(null);
180        hierarchicalDesc = in.readString();
181        parent = in.readParcelable(null);
182     }
183
184    public Folder(Cursor cursor) {
185        id = cursor.getInt(UIProvider.FOLDER_ID_COLUMN);
186        uri = Uri.parse(cursor.getString(UIProvider.FOLDER_URI_COLUMN));
187        name = cursor.getString(UIProvider.FOLDER_NAME_COLUMN);
188        capabilities = cursor.getInt(UIProvider.FOLDER_CAPABILITIES_COLUMN);
189        // 1 for true, 0 for false.
190        hasChildren = cursor.getInt(UIProvider.FOLDER_HAS_CHILDREN_COLUMN) == 1;
191        syncWindow = cursor.getInt(UIProvider.FOLDER_SYNC_WINDOW_COLUMN);
192        String convList = cursor.getString(UIProvider.FOLDER_CONVERSATION_LIST_URI_COLUMN);
193        conversationListUri = !TextUtils.isEmpty(convList) ? Uri.parse(convList) : null;
194        String childList = cursor.getString(UIProvider.FOLDER_CHILD_FOLDERS_LIST_COLUMN);
195        childFoldersListUri = (hasChildren && !TextUtils.isEmpty(childList)) ? Uri.parse(childList)
196                : null;
197        unreadCount = cursor.getInt(UIProvider.FOLDER_UNREAD_COUNT_COLUMN);
198        totalCount = cursor.getInt(UIProvider.FOLDER_TOTAL_COUNT_COLUMN);
199        String refresh = cursor.getString(UIProvider.FOLDER_REFRESH_URI_COLUMN);
200        refreshUri = !TextUtils.isEmpty(refresh) ? Uri.parse(refresh) : null;
201        syncStatus = cursor.getInt(UIProvider.FOLDER_SYNC_STATUS_COLUMN);
202        lastSyncResult = cursor.getInt(UIProvider.FOLDER_LAST_SYNC_RESULT_COLUMN);
203        type = cursor.getInt(UIProvider.FOLDER_TYPE_COLUMN);
204        iconResId = cursor.getLong(UIProvider.FOLDER_ICON_RES_ID_COLUMN);
205        bgColor = cursor.getString(UIProvider.FOLDER_BG_COLOR_COLUMN);
206        fgColor = cursor.getString(UIProvider.FOLDER_FG_COLOR_COLUMN);
207        String loadMore = cursor.getString(UIProvider.FOLDER_LOAD_MORE_URI_COLUMN);
208        loadMoreUri = !TextUtils.isEmpty(loadMore) ? Uri.parse(loadMore) : null;
209        hierarchicalDesc = cursor.getString(UIProvider.FOLDER_HIERARCHICAL_DESC_COLUMN);
210        parent = null;
211    }
212
213    @Override
214    public void writeToParcel(Parcel dest, int flags) {
215        dest.writeInt(id);
216        dest.writeParcelable(uri, 0);
217        dest.writeString(name);
218        dest.writeInt(capabilities);
219        // 1 for true, 0 for false.
220        dest.writeInt(hasChildren ? 1 : 0);
221        dest.writeInt(syncWindow);
222        dest.writeParcelable(conversationListUri, 0);
223        dest.writeParcelable(childFoldersListUri, 0);
224        dest.writeInt(unreadCount);
225        dest.writeInt(totalCount);
226        dest.writeParcelable(refreshUri, 0);
227        dest.writeInt(syncStatus);
228        dest.writeInt(lastSyncResult);
229        dest.writeInt(type);
230        dest.writeLong(iconResId);
231        dest.writeString(bgColor);
232        dest.writeString(fgColor);
233        dest.writeParcelable(loadMoreUri, 0);
234        dest.writeString(hierarchicalDesc);
235        dest.writeParcelable(parent, 0);
236    }
237
238    /**
239     * Construct a folder that queries for search results. Do not call on the UI
240     * thread.
241     */
242    public static CursorLoader forSearchResults(Account account, String query, Context context) {
243        if (account.searchUri != null) {
244            Builder searchBuilder = account.searchUri.buildUpon();
245            searchBuilder.appendQueryParameter(UIProvider.SearchQueryParameters.QUERY, query);
246            Uri searchUri = searchBuilder.build();
247            return new CursorLoader(context, searchUri, UIProvider.FOLDERS_PROJECTION, null, null,
248                    null);
249        }
250        return null;
251    }
252
253    public static HashMap<Uri, Folder> hashMapForFolders(ArrayList<Folder> rawFolders) {
254        final HashMap<Uri, Folder> folders = new HashMap<Uri, Folder>();
255        for (Folder f : rawFolders) {
256            folders.put(f.uri, f);
257        }
258        return folders;
259    }
260
261    private static Uri getValidUri(String uri) {
262        if (TextUtils.isEmpty(uri)) {
263            return null;
264        }
265        return Uri.parse(uri);
266    }
267
268    /**
269     * Constructor that leaves everything uninitialized. For use only by {@link #serialize()}
270     * which is responsible for filling in all the fields
271     */
272    public Folder() {
273        name = FOLDER_UNINITIALIZED;
274    }
275
276    @SuppressWarnings("hiding")
277    public static final Creator<Folder> CREATOR = new Creator<Folder>() {
278        @Override
279        public Folder createFromParcel(Parcel source) {
280            return new Folder(source);
281        }
282
283        @Override
284        public Folder[] newArray(int size) {
285            return new Folder[size];
286        }
287    };
288
289    @Override
290    public int describeContents() {
291        // Return a sort of version number for this parcelable folder. Starting with zero.
292        return 0;
293    }
294
295    @Override
296    public boolean equals(Object o) {
297        if (o == null || !(o instanceof Folder)) {
298            return false;
299        }
300        return Objects.equal(uri, ((Folder) o).uri);
301    }
302
303    @Override
304    public int hashCode() {
305        return uri == null ? 0 : uri.hashCode();
306    }
307
308    @Override
309    public int compareTo(Folder other) {
310        return name.compareToIgnoreCase(other.name);
311    }
312
313    /**
314     * Create a Folder map from a string of serialized folders. This can only be done on the output
315     * of {@link #serialize(Map)}.
316     * @param serializedFolder A string obtained from {@link #serialize(Map)}
317     * @return a Map of folder name to folder.
318     */
319    public static Map<String, Folder> parseFoldersFromString(String serializedFoldersString) {
320        if (TextUtils.isEmpty(serializedFoldersString)) {
321            return null;
322        }
323        Map<String, Folder> folderMap = Maps.newHashMap();
324        ArrayList<Folder> folders = Folder.getFoldersArray(serializedFoldersString);
325        for (Folder folder : folders) {
326            if (folder.name != FOLDER_UNINITIALIZED) {
327                folderMap.put(folder.name, folder);
328            }
329        }
330        return folderMap;
331    }
332
333    /**
334     * Returns a boolean indicating whether network activity (sync) is occuring for this folder.
335     */
336    public boolean isSyncInProgress() {
337        return 0 != (syncStatus & (UIProvider.SyncStatus.BACKGROUND_SYNC |
338                UIProvider.SyncStatus.USER_REFRESH |
339                UIProvider.SyncStatus.USER_QUERY |
340                UIProvider.SyncStatus.USER_MORE_RESULTS));
341    }
342
343    /**
344     * Serialize the given list of folders
345     * @param folderMap A valid map of folder names to Folders
346     * @return a string containing a serialized output of folder maps.
347     */
348    public static String serialize(Map<String, Folder> folderMap) {
349        return getSerializedFolderString(folderMap.values());
350    }
351
352    public boolean supportsCapability(int capability) {
353        return (capabilities & capability) != 0;
354    }
355
356    // Show black text on a transparent swatch for system folders, effectively hiding the
357    // swatch (see bug 2431925).
358    public static void setFolderBlockColor(Folder folder, View colorBlock) {
359        if (colorBlock == null) {
360            return;
361        }
362        final boolean showBg = !TextUtils.isEmpty(folder.bgColor);
363        final int backgroundColor = showBg ? Integer.parseInt(folder.bgColor) : 0;
364        if (!showBg) {
365            colorBlock.setBackgroundDrawable(null);
366            colorBlock.setVisibility(View.GONE);
367        } else {
368            PaintDrawable paintDrawable = new PaintDrawable();
369            paintDrawable.getPaint().setColor(backgroundColor);
370            colorBlock.setBackgroundDrawable(paintDrawable);
371            colorBlock.setVisibility(View.VISIBLE);
372        }
373    }
374
375    public static void setIcon(Folder folder, ImageView iconView) {
376        if (iconView == null) {
377            return;
378        }
379        final long icon = folder.iconResId;
380        if (icon > 0) {
381            iconView.setImageResource((int)icon);
382            iconView.setVisibility(View.VISIBLE);
383        } else {
384            iconView.setVisibility(View.INVISIBLE);
385        }
386    }
387
388    /**
389     * Return if the type of the folder matches a provider defined folder.
390     */
391    public static boolean isProviderFolder(Folder folder) {
392        int type = folder.type;
393        return type == UIProvider.FolderType.INBOX ||
394               type == UIProvider.FolderType.DRAFT ||
395               type == UIProvider.FolderType.OUTBOX ||
396               type == UIProvider.FolderType.SENT ||
397               type == UIProvider.FolderType.TRASH ||
398               type == UIProvider.FolderType.SPAM;
399    }
400
401    public int getBackgroundColor(int defaultColor) {
402        return TextUtils.isEmpty(bgColor) ? defaultColor : Integer.parseInt(bgColor);
403    }
404
405    public int getForegroundColor(int defaultColor) {
406        return TextUtils.isEmpty(fgColor) ? defaultColor : Integer.parseInt(fgColor);
407    }
408
409    public static String getSerializedFolderString(Collection<Folder> folders) {
410        final StringBuilder folderList = new StringBuilder();
411        int i = 0;
412        for (Folder folderEntry : folders) {
413            folderList.append(Folder.toString(folderEntry));
414            if (i < folders.size()) {
415                folderList.append(FOLDERS_SPLIT);
416            }
417            i++;
418        }
419        return folderList.toString();
420    }
421
422
423    public static Folder fromString(String inString) {
424        if (TextUtils.isEmpty(inString)) {
425            return null;
426        }
427        Folder f = new Folder();
428        String[] split = TextUtils.split(inString, SPLITTER_REGEX);
429        f.id = Integer.parseInt(split[0]);
430        f.uri = Folder.getValidUri(split[1]);
431        f.name = split[2];
432        f.hasChildren = Integer.parseInt(split[3]) != 0;
433        f.capabilities = Integer.parseInt(split[4]);
434        f.syncWindow = Integer.parseInt(split[5]);
435        f.conversationListUri = getValidUri(split[6]);
436        f.childFoldersListUri = getValidUri(split[7]);
437        f.unreadCount = Integer.parseInt(split[8]);
438        f.totalCount = Integer.parseInt(split[9]);
439        f.refreshUri = getValidUri(split[10]);
440        f.syncStatus = Integer.parseInt(split[11]);
441        f.lastSyncResult = Integer.parseInt(split[12]);
442        f.type = Integer.parseInt(split[13]);
443        f.iconResId = Integer.parseInt(split[14]);
444        f.bgColor = split[15];
445        f.fgColor = split[16];
446        f.loadMoreUri = getValidUri(split[17]);
447        f.hierarchicalDesc = split[18];
448        f.parent = Folder.fromString(split[19]);
449        return f;
450    }
451
452    public static String toString(Folder folderEntry) {
453        StringBuilder builder = new StringBuilder();
454        builder.append(folderEntry.id);
455        builder.append(SPLITTER);
456        builder.append(folderEntry.uri);
457        builder.append(SPLITTER);
458        builder.append(folderEntry.name);
459        builder.append(SPLITTER);
460        builder.append(folderEntry.hasChildren ? 1 : 0);
461        builder.append(SPLITTER);
462        builder.append(folderEntry.capabilities);
463        builder.append(SPLITTER);
464        builder.append(folderEntry.syncWindow);
465        builder.append(SPLITTER);
466        builder.append(folderEntry.conversationListUri);
467        builder.append(SPLITTER);
468        builder.append(folderEntry.childFoldersListUri);
469        builder.append(SPLITTER);
470        builder.append(folderEntry.unreadCount);
471        builder.append(SPLITTER);
472        builder.append(folderEntry.totalCount);
473        builder.append(SPLITTER);
474        builder.append(folderEntry.refreshUri);
475        builder.append(SPLITTER);
476        builder.append(folderEntry.syncStatus);
477        builder.append(SPLITTER);
478        builder.append(folderEntry.lastSyncResult);
479        builder.append(SPLITTER);
480        builder.append(folderEntry.type);
481        builder.append(SPLITTER);
482        builder.append(folderEntry.iconResId);
483        builder.append(SPLITTER);
484        builder.append(folderEntry.bgColor);
485        builder.append(SPLITTER);
486        builder.append(folderEntry.fgColor);
487        builder.append(SPLITTER);
488        builder.append(folderEntry.loadMoreUri);
489        builder.append(SPLITTER);
490        builder.append(folderEntry.hierarchicalDesc);
491        builder.append(SPLITTER);
492        if (folderEntry.parent != null) {
493            builder.append(Folder.toString(folderEntry.parent));
494        } else {
495            builder.append("");
496        }
497        return builder.toString();
498    }
499
500    /**
501     * Returns a comma separated list of folder URIs for all the folders in the collection.
502     * @param folders
503     * @return
504     */
505    public final static String getUriString(Collection<Folder> folders) {
506        final StringBuilder uris = new StringBuilder();
507        boolean first = true;
508        for (Folder f : folders) {
509            if (first) {
510                first = false;
511            } else {
512                uris.append(',');
513            }
514            uris.append(f.uri.toString());
515        }
516        return uris.toString();
517    }
518
519
520    /**
521     * Get an array of folders from a rawFolders string.
522     */
523    public static ArrayList<Folder> getFoldersArray(String rawFolders) {
524        if (TextUtils.isEmpty(rawFolders)) {
525            return null;
526        }
527        ArrayList<Folder> folders = new ArrayList<Folder>();
528        String[] split = TextUtils.split(rawFolders, FOLDERS_SPLIT_REGEX);
529        for (String folder : split) {
530            folders.add(Folder.fromString(folder));
531        }
532        return folders;
533    }
534
535    /**
536     * Get just the uri's from an arraylist of folders.
537     */
538    public final static String[] getUriArray(ArrayList<Folder> folders) {
539        String[] folderUris = new String[folders.size()];
540        int i = 0;
541        for (Folder folder : folders) {
542            folderUris[i] = folder.uri.toString();
543            i++;
544        }
545        return folderUris;
546    }
547
548    /**
549     * Returns true if a conversation assigned to the needle will be assigned to the collection of
550     * folders in the haystack. False otherwise. This method is safe to call with null
551     * arguments.
552     * This method returns true under two circumstances
553     * <ul><li> If the URI of the needle was found in the collection of URIs that comprise the
554     * haystack.
555     * </li><li> If the needle is of the type Inbox, and at least one of the folders in the haystack
556     * are of type Inbox. <em>Rationale</em>: there are special folders that are marked as inbox,
557     * and the user might not have the control to assign conversations to them. This happens for
558     * the Priority Inbox in Gmail. When you assign a conversation to an Inbox folder, it will
559     * continue to appear in the Priority Inbox. However, the URI of Priority Inbox and Inbox will
560     * be different. So a direct equality check is insufficient.
561     * </li></ul>
562     * @param haystack a collection of folders, possibly overlapping
563     * @param needle a folder
564     * @return true if a conversation inside the needle will be in the folders in the haystack.
565     */
566    public final static boolean containerIncludes(Collection<Folder> haystack, Folder needle) {
567        // If the haystack is empty, it cannot contain anything.
568        if (haystack == null || haystack.size() <= 0) {
569            return false;
570        }
571        // The null folder exists everywhere.
572        if (needle == null) {
573            return true;
574        }
575        boolean hasInbox = false;
576        // Get currently active folder info and compare it to the list
577        // these conversations have been given; if they no longer contain
578        // the selected folder, delete them from the list.
579        final Uri toFind = needle.uri;
580        for (Folder f : haystack) {
581            if (toFind.equals(f.uri)) {
582                return true;
583            }
584            hasInbox |= (f.type == UIProvider.FolderType.INBOX);
585        }
586        // Did not find the URI of needle directly. If the needle is an Inbox and one of the folders
587        // was an inbox, then the needle is contained (check Javadoc for explanation).
588        final boolean needleIsInbox = (needle.type == UIProvider.FolderType.INBOX);
589        return needleIsInbox ? hasInbox : false;
590    }
591
592    /**
593     * Returns a collection of a single folder. This method always returns a valid collection
594     * even if the input folder is null.
595     * @param in a folder, possibly null.
596     * @return a collection of the folder.
597     */
598    public static Collection<Folder> listOf(Folder in) {
599        final Collection<Folder> target = (in == null) ? EMPTY : ImmutableList.of(in);
600        return target;
601    }
602}