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