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