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