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