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