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