Folder.java revision 8eca4db9069941d2b8afbe6f75e3af75a4310f27
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 final String SPLITTER = "^*^";
156    private static final Pattern SPLITTER_REGEX = Pattern.compile("\\^\\*\\^");
157    public static final String FOLDERS_SPLIT = "^**^";
158    private static final 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 boolean isProviderFolder() {
392        return type != UIProvider.FolderType.DEFAULT;
393    }
394
395    public int getBackgroundColor(int defaultColor) {
396        return TextUtils.isEmpty(bgColor) ? defaultColor : Integer.parseInt(bgColor);
397    }
398
399    public int getForegroundColor(int defaultColor) {
400        return TextUtils.isEmpty(fgColor) ? defaultColor : Integer.parseInt(fgColor);
401    }
402
403    public static String getSerializedFolderString(Collection<Folder> folders) {
404        final StringBuilder folderList = new StringBuilder();
405        int i = 0;
406        for (Folder folderEntry : folders) {
407            folderList.append(Folder.toString(folderEntry));
408            if (i < folders.size()) {
409                folderList.append(FOLDERS_SPLIT);
410            }
411            i++;
412        }
413        return folderList.toString();
414    }
415
416
417    public static Folder fromString(String inString) {
418        if (TextUtils.isEmpty(inString)) {
419            return null;
420        }
421        Folder f = new Folder();
422        String[] split = TextUtils.split(inString, SPLITTER_REGEX);
423        if (split.length < 20) {
424            return null;
425        }
426        f.id = Integer.parseInt(split[0]);
427        f.uri = Folder.getValidUri(split[1]);
428        f.name = split[2];
429        f.hasChildren = Integer.parseInt(split[3]) != 0;
430        f.capabilities = Integer.parseInt(split[4]);
431        f.syncWindow = Integer.parseInt(split[5]);
432        f.conversationListUri = getValidUri(split[6]);
433        f.childFoldersListUri = getValidUri(split[7]);
434        f.unreadCount = Integer.parseInt(split[8]);
435        f.totalCount = Integer.parseInt(split[9]);
436        f.refreshUri = getValidUri(split[10]);
437        f.syncStatus = Integer.parseInt(split[11]);
438        f.lastSyncResult = Integer.parseInt(split[12]);
439        f.type = Integer.parseInt(split[13]);
440        f.iconResId = Integer.parseInt(split[14]);
441        f.bgColor = split[15];
442        f.fgColor = split[16];
443        f.loadMoreUri = getValidUri(split[17]);
444        f.hierarchicalDesc = split[18];
445        f.parent = Folder.fromString(split[19]);
446        return f;
447    }
448
449    public static String toString(Folder folderEntry) {
450        StringBuilder builder = new StringBuilder();
451        builder.append(folderEntry.id);
452        builder.append(SPLITTER);
453        builder.append(folderEntry.uri);
454        builder.append(SPLITTER);
455        builder.append(folderEntry.name);
456        builder.append(SPLITTER);
457        builder.append(folderEntry.hasChildren ? 1 : 0);
458        builder.append(SPLITTER);
459        builder.append(folderEntry.capabilities);
460        builder.append(SPLITTER);
461        builder.append(folderEntry.syncWindow);
462        builder.append(SPLITTER);
463        builder.append(folderEntry.conversationListUri);
464        builder.append(SPLITTER);
465        builder.append(folderEntry.childFoldersListUri);
466        builder.append(SPLITTER);
467        builder.append(folderEntry.unreadCount);
468        builder.append(SPLITTER);
469        builder.append(folderEntry.totalCount);
470        builder.append(SPLITTER);
471        builder.append(folderEntry.refreshUri);
472        builder.append(SPLITTER);
473        builder.append(folderEntry.syncStatus);
474        builder.append(SPLITTER);
475        builder.append(folderEntry.lastSyncResult);
476        builder.append(SPLITTER);
477        builder.append(folderEntry.type);
478        builder.append(SPLITTER);
479        builder.append(folderEntry.iconResId);
480        builder.append(SPLITTER);
481        builder.append(folderEntry.bgColor);
482        builder.append(SPLITTER);
483        builder.append(folderEntry.fgColor);
484        builder.append(SPLITTER);
485        builder.append(folderEntry.loadMoreUri);
486        builder.append(SPLITTER);
487        builder.append(folderEntry.hierarchicalDesc);
488        builder.append(SPLITTER);
489        if (folderEntry.parent != null) {
490            builder.append(Folder.toString(folderEntry.parent));
491        } else {
492            builder.append("");
493        }
494        return builder.toString();
495    }
496
497    /**
498     * Returns a comma separated list of folder URIs for all the folders in the collection.
499     * @param folders
500     * @return
501     */
502    public final static String getUriString(Collection<Folder> folders) {
503        final StringBuilder uris = new StringBuilder();
504        boolean first = true;
505        for (Folder f : folders) {
506            if (first) {
507                first = false;
508            } else {
509                uris.append(',');
510            }
511            uris.append(f.uri.toString());
512        }
513        return uris.toString();
514    }
515
516
517    /**
518     * Get an array of folders from a rawFolders string.
519     */
520    public static ArrayList<Folder> getFoldersArray(String rawFolders) {
521        if (TextUtils.isEmpty(rawFolders)) {
522            return null;
523        }
524        ArrayList<Folder> folders = new ArrayList<Folder>();
525        String[] split = TextUtils.split(rawFolders, FOLDERS_SPLIT_REGEX);
526        for (String folder : split) {
527            folders.add(Folder.fromString(folder));
528        }
529        return folders;
530    }
531
532    /**
533     * Get just the uri's from an arraylist of folders.
534     */
535    public final static String[] getUriArray(ArrayList<Folder> folders) {
536        String[] folderUris = new String[folders.size()];
537        int i = 0;
538        for (Folder folder : folders) {
539            folderUris[i] = folder.uri.toString();
540            i++;
541        }
542        return folderUris;
543    }
544
545    /**
546     * Returns true if a conversation assigned to the needle will be assigned to the collection of
547     * folders in the haystack. False otherwise. This method is safe to call with null
548     * arguments.
549     * This method returns true under two circumstances
550     * <ul><li> If the URI of the needle was found in the collection of URIs that comprise the
551     * haystack.
552     * </li><li> If the needle is of the type Inbox, and at least one of the folders in the haystack
553     * are of type Inbox. <em>Rationale</em>: there are special folders that are marked as inbox,
554     * and the user might not have the control to assign conversations to them. This happens for
555     * the Priority Inbox in Gmail. When you assign a conversation to an Inbox folder, it will
556     * continue to appear in the Priority Inbox. However, the URI of Priority Inbox and Inbox will
557     * be different. So a direct equality check is insufficient.
558     * </li></ul>
559     * @param haystack a collection of folders, possibly overlapping
560     * @param needle a folder
561     * @return true if a conversation inside the needle will be in the folders in the haystack.
562     */
563    public final static boolean containerIncludes(Collection<Folder> haystack, Folder needle) {
564        // If the haystack is empty, it cannot contain anything.
565        if (haystack == null || haystack.size() <= 0) {
566            return false;
567        }
568        // The null folder exists everywhere.
569        if (needle == null) {
570            return true;
571        }
572        boolean hasInbox = false;
573        // Get currently active folder info and compare it to the list
574        // these conversations have been given; if they no longer contain
575        // the selected folder, delete them from the list.
576        final Uri toFind = needle.uri;
577        for (Folder f : haystack) {
578            if (toFind.equals(f.uri)) {
579                return true;
580            }
581            hasInbox |= (f.type == UIProvider.FolderType.INBOX);
582        }
583        // Did not find the URI of needle directly. If the needle is an Inbox and one of the folders
584        // was an inbox, then the needle is contained (check Javadoc for explanation).
585        final boolean needleIsInbox = (needle.type == UIProvider.FolderType.INBOX);
586        return needleIsInbox ? hasInbox : false;
587    }
588
589    /**
590     * Returns a collection of a single folder. This method always returns a valid collection
591     * even if the input folder is null.
592     * @param in a folder, possibly null.
593     * @return a collection of the folder.
594     */
595    public static Collection<Folder> listOf(Folder in) {
596        final Collection<Folder> target = (in == null) ? EMPTY : ImmutableList.of(in);
597        return target;
598    }
599}