ConversationListAdapter.java revision 70c73e05a792832aa28da751cdaf3fa83a7b8113
1/*
2 * Copyright (C) 2008 Esmertec AG.
3 * Copyright (C) 2008 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.mms.ui;
19
20import com.android.mms.R;
21import com.android.mms.data.Conversation;
22import com.android.mms.util.ContactInfoCache;
23
24import android.content.Context;
25import android.database.Cursor;
26import android.text.TextUtils;
27import android.util.Log;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.ViewGroup;
31import android.widget.CursorAdapter;
32
33import java.util.Map;
34import java.util.Stack;
35import java.util.concurrent.ConcurrentHashMap;
36import java.util.concurrent.ScheduledThreadPoolExecutor;
37
38/**
39 * The back-end data adapter for ConversationList.
40 */
41//TODO: This should be public class ConversationListAdapter extends ArrayAdapter<Conversation>
42public class ConversationListAdapter extends CursorAdapter {
43    private static final String TAG = "ConversationListAdapter";
44    private static final boolean LOCAL_LOGV = false;
45
46    private final LayoutInflater mFactory;
47
48    // Cache of space-separated recipient ids of a thread to the final
49    // display version.
50
51    // TODO: if you rename a contact or something, it'll cache the old
52    // name (or raw number) forever in here, never listening to
53    // changes from the contacts provider.  We should instead move away
54    // towards using only the CachingNameStore, which does respect
55    // contacts provider updates.
56    private final Map<String, String> mThreadDisplayFrom;
57
58    // For async loading of display names.
59    private final ScheduledThreadPoolExecutor mAsyncLoader;
60    private final Stack<Runnable> mThingsToLoad = new Stack<Runnable>();
61    // We execute things in LIFO order, so as users scroll around during loading,
62    // they get the most recently-requested item.
63    private final Runnable mPopStackRunnable = new Runnable() {
64            public void run() {
65                Runnable r = null;
66                synchronized (mThingsToLoad) {
67                    if (!mThingsToLoad.empty()) {
68                        r = mThingsToLoad.pop();
69                    }
70                }
71                if (r != null) {
72                    r.run();
73                }
74            }
75        };
76
77    private final ConversationList.CachingNameStore mCachingNameStore;
78
79    public ConversationListAdapter(Context context, Cursor cursor,
80                                   ConversationList.CachingNameStore nameStore) {
81        super(context, cursor, true /* auto-requery */);
82        mFactory = LayoutInflater.from(context);
83        mCachingNameStore = nameStore;
84
85        mThreadDisplayFrom = new ConcurrentHashMap<String, String>();
86        // 1 thread.  SQLite can't do better anyway.
87        mAsyncLoader = new ScheduledThreadPoolExecutor(1);
88    }
89
90    /**
91     * Returns the from text using the CachingNameStore.
92     */
93    private String getFromTextFromCache(String spaceSeparatedRcptIds, String address) {
94        // Potentially blocking call to Contacts provider, lookup up
95        // names:  (should usually be cached, though)
96        String value = mCachingNameStore.getContactNames(address);
97
98        if (TextUtils.isEmpty(value)) {
99            value = mContext.getString(R.string.anonymous_recipient);
100        }
101
102        mThreadDisplayFrom.put(spaceSeparatedRcptIds, value);
103        return value;
104    }
105
106    /**
107     * Returns cached 'from' text of message thread (display form of list of recipients)
108     */
109    private String getFromTextFromMessageThread(String spaceSeparatedRcptIds) {
110        // Thread IDs could in-theory be reassigned to different
111        // recipients (if latest threadid was deleted and new
112        // auto-increment was assigned), so our cache key is the
113        // space-separated list of recipients IDs instead:
114        return mThreadDisplayFrom.get(spaceSeparatedRcptIds);
115    }
116
117    @Override
118    public void bindView(View view, Context context, Cursor cursor) {
119        if (!(view instanceof ConversationHeaderView)) {
120            Log.e(TAG, "Unexpected bound view: " + view);
121            return;
122        }
123
124        ConversationHeaderView headerView = (ConversationHeaderView) view;
125        Conversation conv = Conversation.from(context, cursor);
126
127        boolean cacheEntryInvalid = true;
128        int presenceIconResId = 0;
129        String spaceSeparatedRcptIds = conv.getRecipientIds();
130        String from = getFromTextFromMessageThread(spaceSeparatedRcptIds);
131
132        // display the presence from the cache. The cache entry could be invalidated
133        // in the activity's onResume(), but display the info anyways if it's in the cache.
134        // If it's invalid, we'll force a refresh in the async thread.
135        ContactInfoCache.CacheEntry entry = conv.getContactInfo(false);
136        if (entry != null) {
137            presenceIconResId = entry.presenceResId;
138            cacheEntryInvalid = entry.isStale();
139        }
140
141        if (LOCAL_LOGV) Log.v(TAG, "pre-create ConversationHeader");
142
143        ConversationHeader ch = new ConversationHeader(context, conv, from);
144        headerView.bind(context, ch);
145        headerView.setPresenceIcon(presenceIconResId);
146
147        // if the cache entry is invalid, or if we can't find the "from" field,
148        // kick off an async op to refresh the name and presence
149        if (cacheEntryInvalid || (from == null && spaceSeparatedRcptIds != null)) {
150            startAsyncDisplayFromLoad(context, ch, headerView, spaceSeparatedRcptIds);
151        }
152        if (LOCAL_LOGV) Log.v(TAG, "post-bind ConversationHeader");
153    }
154
155    private void startAsyncDisplayFromLoad(final Context context,
156                                           final ConversationHeader ch,
157                                           final ConversationHeaderView headerView,
158                                           final String spaceSeparatedRcptIds) {
159        synchronized (mThingsToLoad) {
160            mThingsToLoad.push(new Runnable() {
161                    public void run() {
162                        String addresses = MessageUtils.getRecipientsByIds(
163                                context, spaceSeparatedRcptIds, true /* allow query */);
164
165                        // set from text
166                        String fromText = getFromTextFromMessageThread(spaceSeparatedRcptIds);
167                        if (TextUtils.isEmpty(fromText)) {
168                            fromText = getFromTextFromCache(spaceSeparatedRcptIds, addresses);
169                        }
170
171                        int presenceIconResId = 0;
172
173                        if (addresses != null && addresses.indexOf(';') < 0) {
174                            // only set presence for single recipient
175                            ContactInfoCache.CacheEntry entry = null;
176                            ContactInfoCache cache = ContactInfoCache.getInstance();
177                            String address = addresses;
178
179                            entry = cache.getContactInfo(address, true);
180
181                            if (entry != null) {
182                                presenceIconResId = entry.presenceResId;
183                            }
184
185                            if (LOCAL_LOGV) {
186                                Log.d(TAG, "ConvListAdapter.startAsyncDisplayFromLoad: " + fromText
187                                    + ", presence=" + presenceIconResId + ", cacheEntry=" + entry);
188                            }
189                        }
190
191                        // need to update the from text and presence icon using a callback, so
192                        // they are done in the UI thread
193                        ch.setFromAndPresence(fromText, presenceIconResId);
194                    }
195                });
196        }
197        mAsyncLoader.execute(mPopStackRunnable);
198    }
199
200    @Override
201    public View newView(Context context, Cursor cursor, ViewGroup parent) {
202        if (LOCAL_LOGV) Log.v(TAG, "inflating new view");
203        return mFactory.inflate(R.layout.conversation_header, parent, false);
204    }
205
206    @Override
207    public void changeCursor(Cursor cursor) {
208        // Now that we are requerying, bindView will restart anything
209        // that might have been pending in the async loader, so clear
210        // out its job stack and let it start fresh.
211        synchronized (mThingsToLoad) {
212            mThingsToLoad.clear();
213        }
214
215        super.changeCursor(cursor);
216    }
217
218    public void invalidateAddressCache() {
219        mThreadDisplayFrom.clear();
220    }
221}
222