1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.dialer.contactinfo; 18 19 import android.os.Handler; 20 import android.os.Message; 21 import android.text.TextUtils; 22 23 import com.android.dialer.calllog.ContactInfo; 24 import com.android.dialer.calllog.ContactInfoHelper; 25 import com.android.dialer.util.ExpirableCache; 26 import com.google.common.annotations.VisibleForTesting; 27 28 import java.util.LinkedList; 29 30 /** 31 * This is a cache of contact details for the phone numbers in the c all log. The key is the 32 * phone number with the country in which teh call was placed or received. The content of the 33 * cache is expired (but not purged) whenever the application comes to the foreground. 34 * 35 * This cache queues request for information and queries for information on a background thread, 36 * so {@code start()} and {@code stop()} must be called to initiate or halt that thread's exeuction 37 * as needed. 38 * 39 * TODO: Explore whether there is a pattern to remove external dependencies for starting and 40 * stopping the query thread. 41 */ 42 public class ContactInfoCache { 43 public interface OnContactInfoChangedListener { 44 public void onContactInfoChanged(); 45 } 46 47 /* 48 * Handles requests for contact name and number type. 49 */ 50 private class QueryThread extends Thread { 51 private volatile boolean mDone = false; 52 53 public QueryThread() { 54 super("ContactInfoCache.QueryThread"); 55 } 56 57 public void stopProcessing() { 58 mDone = true; 59 } 60 61 @Override 62 public void run() { 63 boolean needRedraw = false; 64 while (true) { 65 // Check if thread is finished, and if so return immediately. 66 if (mDone) return; 67 68 // Obtain next request, if any is available. 69 // Keep synchronized section small. 70 ContactInfoRequest req = null; 71 synchronized (mRequests) { 72 if (!mRequests.isEmpty()) { 73 req = mRequests.removeFirst(); 74 } 75 } 76 77 if (req != null) { 78 // Process the request. If the lookup succeeds, schedule a redraw. 79 needRedraw |= queryContactInfo(req.number, req.countryIso, req.callLogInfo); 80 } else { 81 // Throttle redraw rate by only sending them when there are 82 // more requests. 83 if (needRedraw) { 84 needRedraw = false; 85 mHandler.sendEmptyMessage(REDRAW); 86 } 87 88 // Wait until another request is available, or until this 89 // thread is no longer needed (as indicated by being 90 // interrupted). 91 try { 92 synchronized (mRequests) { 93 mRequests.wait(1000); 94 } 95 } catch (InterruptedException ie) { 96 // Ignore, and attempt to continue processing requests. 97 } 98 } 99 } 100 } 101 } 102 103 private Handler mHandler = new Handler() { 104 @Override 105 public void handleMessage(Message msg) { 106 switch (msg.what) { 107 case REDRAW: 108 mOnContactInfoChangedListener.onContactInfoChanged(); 109 break; 110 case START_THREAD: 111 startRequestProcessing(); 112 break; 113 } 114 } 115 }; 116 117 private static final int REDRAW = 1; 118 private static final int START_THREAD = 2; 119 120 private static final int CONTACT_INFO_CACHE_SIZE = 100; 121 private static final int START_PROCESSING_REQUESTS_DELAY_MS = 1000; 122 123 124 /** 125 * List of requests to update contact details. Each request contains a phone number to look up, 126 * and the contact info currently stored in the call log for this number. 127 * 128 * The requests are added when displaying contacts and are processed by a background thread. 129 */ 130 private final LinkedList<ContactInfoRequest> mRequests; 131 132 private ExpirableCache<NumberWithCountryIso, ContactInfo> mCache; 133 134 private ContactInfoHelper mContactInfoHelper; 135 private QueryThread mContactInfoQueryThread; 136 private OnContactInfoChangedListener mOnContactInfoChangedListener; 137 138 public ContactInfoCache(ContactInfoHelper contactInfoHelper, 139 OnContactInfoChangedListener onContactInfoChangedListener) { 140 mContactInfoHelper = contactInfoHelper; 141 mOnContactInfoChangedListener = onContactInfoChangedListener; 142 143 mRequests = new LinkedList<ContactInfoRequest>(); 144 mCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE); 145 } 146 147 public ContactInfo getValue(String number, String countryIso, ContactInfo cachedContactInfo) { 148 NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); 149 ExpirableCache.CachedValue<ContactInfo> cachedInfo = 150 mCache.getCachedValue(numberCountryIso); 151 ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue(); 152 if (cachedInfo == null) { 153 mCache.put(numberCountryIso, ContactInfo.EMPTY); 154 // Use the cached contact info from the call log. 155 info = cachedContactInfo; 156 // The db request should happen on a non-UI thread. 157 // Request the contact details immediately since they are currently missing. 158 enqueueRequest(number, countryIso, cachedContactInfo, true); 159 // We will format the phone number when we make the background request. 160 } else { 161 if (cachedInfo.isExpired()) { 162 // The contact info is no longer up to date, we should request it. However, we 163 // do not need to request them immediately. 164 enqueueRequest(number, countryIso, cachedContactInfo, false); 165 } else if (!callLogInfoMatches(cachedContactInfo, info)) { 166 // The call log information does not match the one we have, look it up again. 167 // We could simply update the call log directly, but that needs to be done in a 168 // background thread, so it is easier to simply request a new lookup, which will, as 169 // a side-effect, update the call log. 170 enqueueRequest(number, countryIso, cachedContactInfo, false); 171 } 172 173 if (info == ContactInfo.EMPTY) { 174 // Use the cached contact info from the call log. 175 info = cachedContactInfo; 176 } 177 } 178 return info; 179 } 180 181 /** 182 * Queries the appropriate content provider for the contact associated with the number. 183 * 184 * Upon completion it also updates the cache in the call log, if it is different from 185 * {@code callLogInfo}. 186 * 187 * The number might be either a SIP address or a phone number. 188 * 189 * It returns true if it updated the content of the cache and we should therefore tell the 190 * view to update its content. 191 */ 192 private boolean queryContactInfo(String number, String countryIso, ContactInfo callLogInfo) { 193 final ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso); 194 195 if (info == null) { 196 // The lookup failed, just return without requesting to update the view. 197 return false; 198 } 199 200 // Check the existing entry in the cache: only if it has changed we should update the 201 // view. 202 NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); 203 ContactInfo existingInfo = mCache.getPossiblyExpired(numberCountryIso); 204 205 final boolean isRemoteSource = info.sourceType != 0; 206 207 // Don't force redraw if existing info in the cache is equal to {@link ContactInfo#EMPTY} 208 // to avoid updating the data set for every new row that is scrolled into view. 209 // see (https://googleplex-android-review.git.corp.google.com/#/c/166680/) 210 211 // Exception: Photo uris for contacts from remote sources are not cached in the call log 212 // cache, so we have to force a redraw for these contacts regardless. 213 boolean updated = (existingInfo != ContactInfo.EMPTY || isRemoteSource) && 214 !info.equals(existingInfo); 215 216 // Store the data in the cache so that the UI thread can use to display it. Store it 217 // even if it has not changed so that it is marked as not expired. 218 mCache.put(numberCountryIso, info); 219 220 // Update the call log even if the cache it is up-to-date: it is possible that the cache 221 // contains the value from a different call log entry. 222 mContactInfoHelper.updateCallLogContactInfo(number, countryIso, info, callLogInfo); 223 return updated; 224 } 225 226 /** 227 * After a delay, start the thread to begin processing requests. We perform lookups on a 228 * background thread, but this must be called to indicate the thread should be running. 229 */ 230 public void start() { 231 // Schedule a thread-creation message if the thread hasn't been created yet, as an 232 // optimization to queue fewer messages. 233 if (mContactInfoQueryThread == null) { 234 // TODO: Check whether this delay before starting to process is necessary. 235 mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MS); 236 } 237 } 238 239 /** 240 * Stops the thread and clears the queue of messages to process. This cleans up the thread 241 * for lookups so that it is not perpetually running. 242 */ 243 public void stop() { 244 stopRequestProcessing(); 245 } 246 247 /** 248 * Starts a background thread to process contact-lookup requests, unless one 249 * has already been started. 250 */ 251 private synchronized void startRequestProcessing() { 252 // For unit-testing. 253 if (mRequestProcessingDisabled) return; 254 255 // If a thread is already started, don't start another. 256 if (mContactInfoQueryThread != null) { 257 return; 258 } 259 260 mContactInfoQueryThread = new QueryThread(); 261 mContactInfoQueryThread.setPriority(Thread.MIN_PRIORITY); 262 mContactInfoQueryThread.start(); 263 } 264 265 public void invalidate() { 266 mCache.expireAll(); 267 stopRequestProcessing(); 268 } 269 270 /** 271 * Stops the background thread that processes updates and cancels any 272 * pending requests to start it. 273 */ 274 private synchronized void stopRequestProcessing() { 275 // Remove any pending requests to start the processing thread. 276 mHandler.removeMessages(START_THREAD); 277 if (mContactInfoQueryThread != null) { 278 // Stop the thread; we are finished with it. 279 mContactInfoQueryThread.stopProcessing(); 280 mContactInfoQueryThread.interrupt(); 281 mContactInfoQueryThread = null; 282 } 283 } 284 285 /** 286 * Enqueues a request to look up the contact details for the given phone number. 287 * <p> 288 * It also provides the current contact info stored in the call log for this number. 289 * <p> 290 * If the {@code immediate} parameter is true, it will start immediately the thread that looks 291 * up the contact information (if it has not been already started). Otherwise, it will be 292 * started with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MILLIS}. 293 */ 294 protected void enqueueRequest(String number, String countryIso, ContactInfo callLogInfo, 295 boolean immediate) { 296 ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo); 297 synchronized (mRequests) { 298 if (!mRequests.contains(request)) { 299 mRequests.add(request); 300 mRequests.notifyAll(); 301 } 302 } 303 if (immediate) { 304 startRequestProcessing(); 305 } 306 } 307 308 /** 309 * Checks whether the contact info from the call log matches the one from the contacts db. 310 */ 311 private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) { 312 // The call log only contains a subset of the fields in the contacts db. 313 // Only check those. 314 return TextUtils.equals(callLogInfo.name, info.name) 315 && callLogInfo.type == info.type 316 && TextUtils.equals(callLogInfo.label, info.label); 317 } 318 319 private volatile boolean mRequestProcessingDisabled = false; 320 321 /** 322 * Sets whether processing of requests for contact details should be enabled. 323 */ 324 public void disableRequestProcessing() { 325 mRequestProcessingDisabled = true; 326 } 327 328 @VisibleForTesting 329 public void injectContactInfoForTest( 330 String number, String countryIso, ContactInfo contactInfo) { 331 NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); 332 mCache.put(numberCountryIso, contactInfo); 333 } 334 } 335