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