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