MessageCursor.java revision 06c0362f59437f3ea2b5832272fb66158bb4b8c0
17bdc3750454efe59617b7df945eadd7e59bee954Andy Huang/*
27bdc3750454efe59617b7df945eadd7e59bee954Andy Huang * Copyright (C) 2012 Google Inc.
37bdc3750454efe59617b7df945eadd7e59bee954Andy Huang * Licensed to The Android Open Source Project.
47bdc3750454efe59617b7df945eadd7e59bee954Andy Huang *
57bdc3750454efe59617b7df945eadd7e59bee954Andy Huang * Licensed under the Apache License, Version 2.0 (the "License");
67bdc3750454efe59617b7df945eadd7e59bee954Andy Huang * you may not use this file except in compliance with the License.
77bdc3750454efe59617b7df945eadd7e59bee954Andy Huang * You may obtain a copy of the License at
87bdc3750454efe59617b7df945eadd7e59bee954Andy Huang *
97bdc3750454efe59617b7df945eadd7e59bee954Andy Huang *      http://www.apache.org/licenses/LICENSE-2.0
107bdc3750454efe59617b7df945eadd7e59bee954Andy Huang *
117bdc3750454efe59617b7df945eadd7e59bee954Andy Huang * Unless required by applicable law or agreed to in writing, software
127bdc3750454efe59617b7df945eadd7e59bee954Andy Huang * distributed under the License is distributed on an "AS IS" BASIS,
137bdc3750454efe59617b7df945eadd7e59bee954Andy Huang * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
147bdc3750454efe59617b7df945eadd7e59bee954Andy Huang * See the License for the specific language governing permissions and
157bdc3750454efe59617b7df945eadd7e59bee954Andy Huang * limitations under the License.
167bdc3750454efe59617b7df945eadd7e59bee954Andy Huang */
177bdc3750454efe59617b7df945eadd7e59bee954Andy Huang
187bdc3750454efe59617b7df945eadd7e59bee954Andy Huangpackage com.android.mail.browse;
197bdc3750454efe59617b7df945eadd7e59bee954Andy Huang
207bdc3750454efe59617b7df945eadd7e59bee954Andy Huangimport android.database.Cursor;
217bdc3750454efe59617b7df945eadd7e59bee954Andy Huangimport android.database.CursorWrapper;
2247aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huangimport android.os.Bundle;
23839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huangimport android.os.Parcelable;
247bdc3750454efe59617b7df945eadd7e59bee954Andy Huang
256766b6e5468d2f1935587b3bc1f8e65be94cd6fbAndy Huangimport com.android.mail.providers.Attachment;
26839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huangimport com.android.mail.providers.Conversation;
277bdc3750454efe59617b7df945eadd7e59bee954Andy Huangimport com.android.mail.providers.Message;
287bdc3750454efe59617b7df945eadd7e59bee954Andy Huangimport com.android.mail.providers.UIProvider;
2947aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huangimport com.android.mail.providers.UIProvider.CursorExtraKeys;
3047aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huangimport com.android.mail.providers.UIProvider.CursorStatus;
31839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huangimport com.android.mail.ui.ConversationUpdater;
32cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huangimport com.google.common.base.Objects;
33bf232c3735f65b1a4746943e4a134e59e36f0bdePaul Westbrookimport com.google.common.collect.Maps;
347bdc3750454efe59617b7df945eadd7e59bee954Andy Huang
357bdc3750454efe59617b7df945eadd7e59bee954Andy Huangimport java.util.Map;
367bdc3750454efe59617b7df945eadd7e59bee954Andy Huang
377bdc3750454efe59617b7df945eadd7e59bee954Andy Huang/**
387bdc3750454efe59617b7df945eadd7e59bee954Andy Huang * MessageCursor contains the messages within a conversation; the public methods within should
397bdc3750454efe59617b7df945eadd7e59bee954Andy Huang * only be called by the UI thread, as cursor position isn't guaranteed to be maintained
407bdc3750454efe59617b7df945eadd7e59bee954Andy Huang */
417bdc3750454efe59617b7df945eadd7e59bee954Andy Huangpublic class MessageCursor extends CursorWrapper {
427bdc3750454efe59617b7df945eadd7e59bee954Andy Huang
43839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang    private final Map<Long, ConversationMessage> mCache = Maps.newHashMap();
44839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang    private final Conversation mConversation;
45cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang    private final ConversationController mController;
467bdc3750454efe59617b7df945eadd7e59bee954Andy Huang
4747aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang    private Integer mStatus;
4847aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang
49cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang    public interface ConversationController {
50cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang        ConversationUpdater getListController();
51cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang        MessageCursor getMessageCursor();
52cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang    }
53cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang
54839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang    /**
55839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang     * A message created as part of a conversation view. Sometimes, like during star/unstar, it's
56cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang     * handy to have the owning {@link Conversation} for context.
57cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang     *
58cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang     * <p>This class must remain separate from the {@link MessageCursor} from whence it came,
59cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang     * because cursors can be closed by their Loaders at any time. The
60cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang     * {@link ConversationController} intermediate is used to obtain the currently opened cursor.
61839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang     *
62839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang     * <p>(N.B. This is a {@link Parcelable}, so try not to add non-transient fields here.
63839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang     * Parcelable state belongs either in {@link Message} or {@link MessageViewState}. The
64839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang     * assumption is that this class never needs the state of its extra context saved.)
65839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang     */
66cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang    public static final class ConversationMessage extends Message {
67839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang
68839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang        public final transient Conversation conversation;
69839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang
70cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang        private final transient ConversationController mController;
71839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang
72839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang        public ConversationMessage(MessageCursor cursor, Conversation conv,
73cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang                ConversationController controller) {
74839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang            super(cursor);
75839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang            conversation = conv;
76cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang            mController = controller;
77cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang        }
78cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang
79cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang        /**
80cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang         * Returns a hash code based on this message's identity, contents and current state.
81cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang         * This is a separate method from hashCode() to allow for an instance of this class to be
82cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang         * a functional key in a hash-based data structure.
83cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang         *
84cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang         */
85cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang        public int getStateHashCode() {
866766b6e5468d2f1935587b3bc1f8e65be94cd6fbAndy Huang            return Objects.hashCode(uri, read, starred, getAttachmentsStateHashCode());
876766b6e5468d2f1935587b3bc1f8e65be94cd6fbAndy Huang        }
886766b6e5468d2f1935587b3bc1f8e65be94cd6fbAndy Huang
896766b6e5468d2f1935587b3bc1f8e65be94cd6fbAndy Huang        private int getAttachmentsStateHashCode() {
906766b6e5468d2f1935587b3bc1f8e65be94cd6fbAndy Huang            int hash = 0;
916766b6e5468d2f1935587b3bc1f8e65be94cd6fbAndy Huang            for (Attachment a : getAttachments()) {
926766b6e5468d2f1935587b3bc1f8e65be94cd6fbAndy Huang                hash += (a.uri != null ? a.uri.hashCode() : 0);
936766b6e5468d2f1935587b3bc1f8e65be94cd6fbAndy Huang            }
946766b6e5468d2f1935587b3bc1f8e65be94cd6fbAndy Huang            return hash;
95839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang        }
96839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang
97839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang        public boolean isConversationStarred() {
98cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang            final MessageCursor c = mController.getMessageCursor();
99cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang            return c != null && c.isConversationStarred();
100839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang        }
101839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang
102839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang        public void star(boolean newStarred) {
103cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang            final ConversationUpdater listController = mController.getListController();
104cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang            if (listController != null) {
105cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang                listController.starMessage(this, newStarred);
106cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang            }
107839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang        }
108839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang
109839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang    }
110839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang
111cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang    public MessageCursor(Cursor inner, Conversation conv, ConversationController controller) {
1127bdc3750454efe59617b7df945eadd7e59bee954Andy Huang        super(inner);
113839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang        mConversation = conv;
114cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang        mController = controller;
1157bdc3750454efe59617b7df945eadd7e59bee954Andy Huang    }
1167bdc3750454efe59617b7df945eadd7e59bee954Andy Huang
117839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang    public ConversationMessage getMessage() {
1187bdc3750454efe59617b7df945eadd7e59bee954Andy Huang        final long id = getWrappedCursor().getLong(UIProvider.MESSAGE_ID_COLUMN);
119839ada22ea84251dde3305003d2f8fc5bf14914eAndy Huang        ConversationMessage m = mCache.get(id);
1207bdc3750454efe59617b7df945eadd7e59bee954Andy Huang        if (m == null) {
121cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang            m = new ConversationMessage(this, mConversation, mController);
1227bdc3750454efe59617b7df945eadd7e59bee954Andy Huang            mCache.put(id, m);
1237bdc3750454efe59617b7df945eadd7e59bee954Andy Huang        }
1247bdc3750454efe59617b7df945eadd7e59bee954Andy Huang        return m;
1257bdc3750454efe59617b7df945eadd7e59bee954Andy Huang    }
126cf164d64bcb1da92b427bda99b97f7ec310ef704Marc Blank
127cf164d64bcb1da92b427bda99b97f7ec310ef704Marc Blank    // Is the conversation starred?
128cf164d64bcb1da92b427bda99b97f7ec310ef704Marc Blank    public boolean isConversationStarred() {
129cf164d64bcb1da92b427bda99b97f7ec310ef704Marc Blank        int pos = -1;
130cf164d64bcb1da92b427bda99b97f7ec310ef704Marc Blank        while (moveToPosition(++pos)) {
131423bea25992492efea7d414819729f9eae7ce72eAndy Huang            if (getMessage().starred) {
132cf164d64bcb1da92b427bda99b97f7ec310ef704Marc Blank                return true;
133cf164d64bcb1da92b427bda99b97f7ec310ef704Marc Blank            }
134cf164d64bcb1da92b427bda99b97f7ec310ef704Marc Blank        }
135cf164d64bcb1da92b427bda99b97f7ec310ef704Marc Blank        return false;
136cf164d64bcb1da92b427bda99b97f7ec310ef704Marc Blank    }
137bf232c3735f65b1a4746943e4a134e59e36f0bdePaul Westbrook
138423bea25992492efea7d414819729f9eae7ce72eAndy Huang
139423bea25992492efea7d414819729f9eae7ce72eAndy Huang    public boolean isConversationRead() {
140423bea25992492efea7d414819729f9eae7ce72eAndy Huang        int pos = -1;
141423bea25992492efea7d414819729f9eae7ce72eAndy Huang        while (moveToPosition(++pos)) {
142423bea25992492efea7d414819729f9eae7ce72eAndy Huang            if (!getMessage().read) {
143423bea25992492efea7d414819729f9eae7ce72eAndy Huang                return false;
144423bea25992492efea7d414819729f9eae7ce72eAndy Huang            }
145423bea25992492efea7d414819729f9eae7ce72eAndy Huang        }
146423bea25992492efea7d414819729f9eae7ce72eAndy Huang        return true;
147423bea25992492efea7d414819729f9eae7ce72eAndy Huang    }
148cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang    public void markMessagesRead() {
149cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang        int pos = -1;
150cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang        while (moveToPosition(++pos)) {
151cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang            getMessage().read = true;
152cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang        }
153cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang    }
154cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang
1556766b6e5468d2f1935587b3bc1f8e65be94cd6fbAndy Huang    public int getStateHashCode() {
15606c0362f59437f3ea2b5832272fb66158bb4b8c0Andy Huang        return getStateHashCode(0);
15706c0362f59437f3ea2b5832272fb66158bb4b8c0Andy Huang    }
15806c0362f59437f3ea2b5832272fb66158bb4b8c0Andy Huang
15906c0362f59437f3ea2b5832272fb66158bb4b8c0Andy Huang    /**
16006c0362f59437f3ea2b5832272fb66158bb4b8c0Andy Huang     * Calculate a hash code that compactly summarizes the state of the messages in this cursor,
16106c0362f59437f3ea2b5832272fb66158bb4b8c0Andy Huang     * with respect to the way the messages are displayed in conversation view. This is not a
16206c0362f59437f3ea2b5832272fb66158bb4b8c0Andy Huang     * general-purpose hash code. When the state hash codes of a new cursor differs from the
16306c0362f59437f3ea2b5832272fb66158bb4b8c0Andy Huang     * existing cursor's hash code, the conversation view will re-render from scratch.
16406c0362f59437f3ea2b5832272fb66158bb4b8c0Andy Huang     *
16506c0362f59437f3ea2b5832272fb66158bb4b8c0Andy Huang     * @param exceptLast optional number of messages to exclude iterating through at the end of the
16606c0362f59437f3ea2b5832272fb66158bb4b8c0Andy Huang     * cursor. pass zero to iterate through all messages (or use {@link #getStateHashCode()}).
16706c0362f59437f3ea2b5832272fb66158bb4b8c0Andy Huang     * @return state hash code of the selected messages in this cursor
16806c0362f59437f3ea2b5832272fb66158bb4b8c0Andy Huang     */
16906c0362f59437f3ea2b5832272fb66158bb4b8c0Andy Huang    public int getStateHashCode(int exceptLast) {
170cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang        int hashCode = 17;
171cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang        int pos = -1;
17206c0362f59437f3ea2b5832272fb66158bb4b8c0Andy Huang        final int stopAt = getCount() - exceptLast;
17306c0362f59437f3ea2b5832272fb66158bb4b8c0Andy Huang        while (moveToPosition(++pos) && pos < stopAt) {
174cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang            hashCode = 31 * hashCode + getMessage().getStateHashCode();
175cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang        }
176cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang        return hashCode;
177cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang    }
178cd5c5eeae167885ffa2959c200233fea2f39c5f7Andy Huang
17947aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang    public int getStatus() {
18047aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang        if (mStatus != null) {
18147aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang            return mStatus;
18247aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang        }
18347aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang
18447aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang        mStatus = CursorStatus.LOADED;
18547aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang        final Bundle extras = getExtras();
18647aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang        if (extras != null && extras.containsKey(CursorExtraKeys.EXTRA_STATUS)) {
18747aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang            mStatus = extras.getInt(CursorExtraKeys.EXTRA_STATUS);
18847aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang        }
18947aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang        return mStatus;
19047aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang    }
19147aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang
19247aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang    public boolean isLoaded() {
19347aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang        return getStatus() >= CursorStatus.LOADED || getCount() > 0; // FIXME: remove count hack
19447aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang    }
19547aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang
19647aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang    public String getDebugDump() {
19747aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang        StringBuilder sb = new StringBuilder();
19847aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang        sb.append(String.format("conv subj='%s' status=%d messages:\n",
19947aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang                mConversation.subject, getStatus()));
20047aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang        int pos = -1;
20147aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang        while (moveToPosition(++pos)) {
2026766b6e5468d2f1935587b3bc1f8e65be94cd6fbAndy Huang            final ConversationMessage m = getMessage();
20347aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang            sb.append(String.format(
2046766b6e5468d2f1935587b3bc1f8e65be94cd6fbAndy Huang                    "[Message #%d hash=%s uri=%s id=%s serverId=%s from='%s' draftType=%d" +
2056766b6e5468d2f1935587b3bc1f8e65be94cd6fbAndy Huang                    " isSending=%s read=%s starred=%s attJson=%s]\n",
2066766b6e5468d2f1935587b3bc1f8e65be94cd6fbAndy Huang                    pos, m.getStateHashCode(), m.uri, m.id, m.serverId, m.from, m.draftType,
2076766b6e5468d2f1935587b3bc1f8e65be94cd6fbAndy Huang                    m.isSending, m.read, m.starred, m.attachmentsJson));
20847aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang        }
20947aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang        return sb.toString();
21047aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang    }
21147aa9c991b33c722a6ba1946fc02e0aba17bc1c9Andy Huang
2127bdc3750454efe59617b7df945eadd7e59bee954Andy Huang}