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