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