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