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