Folder.java revision 4bfce9ada8b2a6d54ec21a11ba78aaaebe6cfc2f
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 UIProvider.SyncStatus.isSyncInProgress(syncStatus);
338    }
339
340    /**
341     * Serialize the given list of folders
342     * @param folderMap A valid map of folder names to Folders
343     * @return a string containing a serialized output of folder maps.
344     */
345    public static String serialize(Map<String, Folder> folderMap) {
346        return getSerializedFolderString(folderMap.values());
347    }
348
349    public boolean supportsCapability(int capability) {
350        return (capabilities & capability) != 0;
351    }
352
353    // Show black text on a transparent swatch for system folders, effectively hiding the
354    // swatch (see bug 2431925).
355    public static void setFolderBlockColor(Folder folder, View colorBlock) {
356        if (colorBlock == null) {
357            return;
358        }
359        final boolean showBg = !TextUtils.isEmpty(folder.bgColor);
360        final int backgroundColor = showBg ? Integer.parseInt(folder.bgColor) : 0;
361        if (!showBg) {
362            colorBlock.setBackgroundDrawable(null);
363            colorBlock.setVisibility(View.GONE);
364        } else {
365            PaintDrawable paintDrawable = new PaintDrawable();
366            paintDrawable.getPaint().setColor(backgroundColor);
367            colorBlock.setBackgroundDrawable(paintDrawable);
368            colorBlock.setVisibility(View.VISIBLE);
369        }
370    }
371
372    public static void setIcon(Folder folder, ImageView iconView) {
373        if (iconView == null) {
374            return;
375        }
376        final long icon = folder.iconResId;
377        if (icon > 0) {
378            iconView.setImageResource((int)icon);
379            iconView.setVisibility(View.VISIBLE);
380        } else {
381            iconView.setVisibility(View.INVISIBLE);
382        }
383    }
384
385    /**
386     * Return if the type of the folder matches a provider defined folder.
387     */
388    public boolean isProviderFolder() {
389        return type != UIProvider.FolderType.DEFAULT;
390    }
391
392    public int getBackgroundColor(int defaultColor) {
393        return TextUtils.isEmpty(bgColor) ? defaultColor : Integer.parseInt(bgColor);
394    }
395
396    public int getForegroundColor(int defaultColor) {
397        return TextUtils.isEmpty(fgColor) ? defaultColor : Integer.parseInt(fgColor);
398    }
399
400    public static String getSerializedFolderString(Collection<Folder> folders) {
401        final StringBuilder folderList = new StringBuilder();
402        int i = 0;
403        for (Folder folderEntry : folders) {
404            folderList.append(Folder.toString(folderEntry));
405            if (i < folders.size()) {
406                folderList.append(FOLDERS_SPLIT);
407            }
408            i++;
409        }
410        return folderList.toString();
411    }
412
413
414    public static Folder fromString(String inString) {
415        if (TextUtils.isEmpty(inString)) {
416            return null;
417        }
418        Folder f = new Folder();
419        String[] split = TextUtils.split(inString, SPLITTER_REGEX);
420        if (split.length < 20) {
421            return null;
422        }
423        f.id = Integer.parseInt(split[0]);
424        f.uri = Folder.getValidUri(split[1]);
425        f.name = split[2];
426        f.hasChildren = Integer.parseInt(split[3]) != 0;
427        f.capabilities = Integer.parseInt(split[4]);
428        f.syncWindow = Integer.parseInt(split[5]);
429        f.conversationListUri = getValidUri(split[6]);
430        f.childFoldersListUri = getValidUri(split[7]);
431        f.unreadCount = Integer.parseInt(split[8]);
432        f.totalCount = Integer.parseInt(split[9]);
433        f.refreshUri = getValidUri(split[10]);
434        f.syncStatus = Integer.parseInt(split[11]);
435        f.lastSyncResult = Integer.parseInt(split[12]);
436        f.type = Integer.parseInt(split[13]);
437        f.iconResId = Integer.parseInt(split[14]);
438        f.bgColor = split[15];
439        f.fgColor = split[16];
440        f.loadMoreUri = getValidUri(split[17]);
441        f.hierarchicalDesc = split[18];
442        f.parent = Folder.fromString(split[19]);
443        return f;
444    }
445
446    public static String toString(Folder folderEntry) {
447        StringBuilder builder = new StringBuilder();
448        builder.append(folderEntry.id);
449        builder.append(SPLITTER);
450        builder.append(folderEntry.uri);
451        builder.append(SPLITTER);
452        builder.append(folderEntry.name);
453        builder.append(SPLITTER);
454        builder.append(folderEntry.hasChildren ? 1 : 0);
455        builder.append(SPLITTER);
456        builder.append(folderEntry.capabilities);
457        builder.append(SPLITTER);
458        builder.append(folderEntry.syncWindow);
459        builder.append(SPLITTER);
460        builder.append(folderEntry.conversationListUri);
461        builder.append(SPLITTER);
462        builder.append(folderEntry.childFoldersListUri);
463        builder.append(SPLITTER);
464        builder.append(folderEntry.unreadCount);
465        builder.append(SPLITTER);
466        builder.append(folderEntry.totalCount);
467        builder.append(SPLITTER);
468        builder.append(folderEntry.refreshUri);
469        builder.append(SPLITTER);
470        builder.append(folderEntry.syncStatus);
471        builder.append(SPLITTER);
472        builder.append(folderEntry.lastSyncResult);
473        builder.append(SPLITTER);
474        builder.append(folderEntry.type);
475        builder.append(SPLITTER);
476        builder.append(folderEntry.iconResId);
477        builder.append(SPLITTER);
478        builder.append(folderEntry.bgColor);
479        builder.append(SPLITTER);
480        builder.append(folderEntry.fgColor);
481        builder.append(SPLITTER);
482        builder.append(folderEntry.loadMoreUri);
483        builder.append(SPLITTER);
484        builder.append(folderEntry.hierarchicalDesc);
485        builder.append(SPLITTER);
486        if (folderEntry.parent != null) {
487            builder.append(Folder.toString(folderEntry.parent));
488        } else {
489            builder.append("");
490        }
491        return builder.toString();
492    }
493
494    /**
495     * Returns a comma separated list of folder URIs for all the folders in the collection.
496     * @param folders
497     * @return
498     */
499    public final static String getUriString(Collection<Folder> folders) {
500        final StringBuilder uris = new StringBuilder();
501        boolean first = true;
502        for (Folder f : folders) {
503            if (first) {
504                first = false;
505            } else {
506                uris.append(',');
507            }
508            uris.append(f.uri.toString());
509        }
510        return uris.toString();
511    }
512
513
514    /**
515     * Get an array of folders from a rawFolders string.
516     */
517    public static ArrayList<Folder> getFoldersArray(String rawFolders) {
518        if (TextUtils.isEmpty(rawFolders)) {
519            return null;
520        }
521        ArrayList<Folder> folders = new ArrayList<Folder>();
522        String[] split = TextUtils.split(rawFolders, FOLDERS_SPLIT_REGEX);
523        Folder f;
524        for (String folder : split) {
525            f = Folder.fromString(folder);
526            if (f != null) {
527                folders.add(f);
528            }
529        }
530        return folders;
531    }
532
533    /**
534     * Get just the uri's from an arraylist of folders.
535     */
536    public final static String[] getUriArray(ArrayList<Folder> folders) {
537        if (folders == null || folders.size() == 0) {
538            return new String[0];
539        }
540        String[] folderUris = new String[folders.size()];
541        int i = 0;
542        for (Folder folder : folders) {
543            folderUris[i] = folder.uri.toString();
544            i++;
545        }
546        return folderUris;
547    }
548
549    /**
550     * Returns true if a conversation assigned to the needle will be assigned to the collection of
551     * folders in the haystack. False otherwise. This method is safe to call with null
552     * arguments.
553     * This method returns true under two circumstances
554     * <ul><li> If the URI of the needle was found in the collection of URIs that comprise the
555     * haystack.
556     * </li><li> If the needle is of the type Inbox, and at least one of the folders in the haystack
557     * are of type Inbox. <em>Rationale</em>: there are special folders that are marked as inbox,
558     * and the user might not have the control to assign conversations to them. This happens for
559     * the Priority Inbox in Gmail. When you assign a conversation to an Inbox folder, it will
560     * continue to appear in the Priority Inbox. However, the URI of Priority Inbox and Inbox will
561     * be different. So a direct equality check is insufficient.
562     * </li></ul>
563     * @param haystack a collection of folders, possibly overlapping
564     * @param needle a folder
565     * @return true if a conversation inside the needle will be in the folders in the haystack.
566     */
567    public final static boolean containerIncludes(Collection<Folder> haystack, Folder needle) {
568        // If the haystack is empty, it cannot contain anything.
569        if (haystack == null || haystack.size() <= 0) {
570            return false;
571        }
572        // The null folder exists everywhere.
573        if (needle == null) {
574            return true;
575        }
576        boolean hasInbox = false;
577        // Get currently active folder info and compare it to the list
578        // these conversations have been given; if they no longer contain
579        // the selected folder, delete them from the list.
580        final Uri toFind = needle.uri;
581        for (Folder f : haystack) {
582            if (toFind.equals(f.uri)) {
583                return true;
584            }
585            hasInbox |= (f.type == UIProvider.FolderType.INBOX);
586        }
587        // Did not find the URI of needle directly. If the needle is an Inbox and one of the folders
588        // was an inbox, then the needle is contained (check Javadoc for explanation).
589        final boolean needleIsInbox = (needle.type == UIProvider.FolderType.INBOX);
590        return needleIsInbox ? hasInbox : false;
591    }
592
593    /**
594     * Returns a boolean indicating whether this Folder object has been initialized
595     */
596    public boolean isInitialized() {
597        return name != FOLDER_UNINITIALIZED;
598    }
599
600    /**
601     * Returns a collection of a single folder. This method always returns a valid collection
602     * even if the input folder is null.
603     * @param in a folder, possibly null.
604     * @return a collection of the folder.
605     */
606    public static Collection<Folder> listOf(Folder in) {
607        final Collection<Folder> target = (in == null) ? EMPTY : ImmutableList.of(in);
608        return target;
609    }
610}
611