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.app.contactinfo; 18 19 import android.os.Handler; 20 import android.os.Message; 21 import android.os.SystemClock; 22 import android.support.annotation.NonNull; 23 import android.support.annotation.VisibleForTesting; 24 import android.text.TextUtils; 25 import com.android.dialer.common.LogUtil; 26 import com.android.dialer.logging.ContactSource.Type; 27 import com.android.dialer.phonenumbercache.ContactInfo; 28 import com.android.dialer.phonenumbercache.ContactInfoHelper; 29 import com.android.dialer.util.ExpirableCache; 30 import java.lang.ref.WeakReference; 31 import java.util.Objects; 32 import java.util.concurrent.BlockingQueue; 33 import java.util.concurrent.PriorityBlockingQueue; 34 35 /** 36 * This is a cache of contact details for the phone numbers in the call log. The key is the phone 37 * number with the country in which the call was placed or received. The content of the cache is 38 * expired (but not purged) whenever the application comes to the foreground. 39 * 40 * <p>This cache queues request for information and queries for information on a background thread, 41 * so {@code start()} and {@code stop()} must be called to initiate or halt that thread's exeuction 42 * as needed. 43 * 44 * <p>TODO: Explore whether there is a pattern to remove external dependencies for starting and 45 * stopping the query thread. 46 */ 47 public class ContactInfoCache { 48 49 private static final int REDRAW = 1; 50 private static final int START_THREAD = 2; 51 private static final int START_PROCESSING_REQUESTS_DELAY_MS = 1000; 52 53 private final ExpirableCache<NumberWithCountryIso, ContactInfo> mCache; 54 private final ContactInfoHelper mContactInfoHelper; 55 private final OnContactInfoChangedListener mOnContactInfoChangedListener; 56 private final BlockingQueue<ContactInfoRequest> mUpdateRequests; 57 private final Handler mHandler; 58 private QueryThread mContactInfoQueryThread; 59 private volatile boolean mRequestProcessingDisabled = false; 60 61 private static class InnerHandler extends Handler { 62 63 private final WeakReference<ContactInfoCache> contactInfoCacheWeakReference; 64 InnerHandler(WeakReference<ContactInfoCache> contactInfoCacheWeakReference)65 public InnerHandler(WeakReference<ContactInfoCache> contactInfoCacheWeakReference) { 66 this.contactInfoCacheWeakReference = contactInfoCacheWeakReference; 67 } 68 69 @Override handleMessage(Message msg)70 public void handleMessage(Message msg) { 71 ContactInfoCache reference = contactInfoCacheWeakReference.get(); 72 if (reference == null) { 73 return; 74 } 75 switch (msg.what) { 76 case REDRAW: 77 reference.mOnContactInfoChangedListener.onContactInfoChanged(); 78 break; 79 case START_THREAD: 80 reference.startRequestProcessing(); 81 break; 82 default: // fall out 83 } 84 } 85 } 86 ContactInfoCache( @onNull ExpirableCache<NumberWithCountryIso, ContactInfo> internalCache, @NonNull ContactInfoHelper contactInfoHelper, @NonNull OnContactInfoChangedListener listener)87 public ContactInfoCache( 88 @NonNull ExpirableCache<NumberWithCountryIso, ContactInfo> internalCache, 89 @NonNull ContactInfoHelper contactInfoHelper, 90 @NonNull OnContactInfoChangedListener listener) { 91 mCache = internalCache; 92 mContactInfoHelper = contactInfoHelper; 93 mOnContactInfoChangedListener = listener; 94 mUpdateRequests = new PriorityBlockingQueue<>(); 95 mHandler = new InnerHandler(new WeakReference<>(this)); 96 } 97 getValue( String number, String countryIso, ContactInfo callLogContactInfo, boolean remoteLookupIfNotFoundLocally)98 public ContactInfo getValue( 99 String number, 100 String countryIso, 101 ContactInfo callLogContactInfo, 102 boolean remoteLookupIfNotFoundLocally) { 103 NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); 104 ExpirableCache.CachedValue<ContactInfo> cachedInfo = mCache.getCachedValue(numberCountryIso); 105 ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue(); 106 int requestType = 107 remoteLookupIfNotFoundLocally 108 ? ContactInfoRequest.TYPE_LOCAL_AND_REMOTE 109 : ContactInfoRequest.TYPE_LOCAL; 110 if (cachedInfo == null) { 111 mCache.put(numberCountryIso, ContactInfo.EMPTY); 112 // Use the cached contact info from the call log. 113 info = callLogContactInfo; 114 // The db request should happen on a non-UI thread. 115 // Request the contact details immediately since they are currently missing. 116 enqueueRequest(number, countryIso, callLogContactInfo, /* immediate */ true, requestType); 117 // We will format the phone number when we make the background request. 118 } else { 119 if (cachedInfo.isExpired()) { 120 // The contact info is no longer up to date, we should request it. However, we 121 // do not need to request them immediately. 122 enqueueRequest(number, countryIso, callLogContactInfo, /* immediate */ false, requestType); 123 } else if (!callLogInfoMatches(callLogContactInfo, info)) { 124 // The call log information does not match the one we have, look it up again. 125 // We could simply update the call log directly, but that needs to be done in a 126 // background thread, so it is easier to simply request a new lookup, which will, as 127 // a side-effect, update the call log. 128 enqueueRequest(number, countryIso, callLogContactInfo, /* immediate */ false, requestType); 129 } 130 131 if (Objects.equals(info, ContactInfo.EMPTY)) { 132 // Use the cached contact info from the call log. 133 info = callLogContactInfo; 134 } 135 } 136 return info; 137 } 138 139 /** 140 * Queries the appropriate content provider for the contact associated with the number. 141 * 142 * <p>Upon completion it also updates the cache in the call log, if it is different from {@code 143 * callLogInfo}. 144 * 145 * <p>The number might be either a SIP address or a phone number. 146 * 147 * <p>It returns true if it updated the content of the cache and we should therefore tell the view 148 * to update its content. 149 */ queryContactInfo(ContactInfoRequest request)150 private boolean queryContactInfo(ContactInfoRequest request) { 151 LogUtil.d( 152 "ContactInfoCache.queryContactInfo", 153 "request number: %s, type: %d", 154 LogUtil.sanitizePhoneNumber(request.number), 155 request.type); 156 ContactInfo info; 157 if (request.isLocalRequest()) { 158 info = mContactInfoHelper.lookupNumber(request.number, request.countryIso); 159 if (!info.contactExists) { 160 // TODO: Maybe skip look up if it's already available in cached number lookup 161 // service. 162 long start = SystemClock.elapsedRealtime(); 163 mContactInfoHelper.updateFromCequintCallerId(info, request.number); 164 long time = SystemClock.elapsedRealtime() - start; 165 LogUtil.d( 166 "ContactInfoCache.queryContactInfo", "Cequint Caller Id look up takes %d ms", time); 167 } 168 if (request.type == ContactInfoRequest.TYPE_LOCAL_AND_REMOTE) { 169 if (!mContactInfoHelper.hasName(info)) { 170 enqueueRequest( 171 request.number, 172 request.countryIso, 173 request.callLogInfo, 174 true, 175 ContactInfoRequest.TYPE_REMOTE); 176 return false; 177 } 178 } 179 } else { 180 info = mContactInfoHelper.lookupNumberInRemoteDirectory(request.number, request.countryIso); 181 } 182 183 if (info == null) { 184 // The lookup failed, just return without requesting to update the view. 185 return false; 186 } 187 188 // Check the existing entry in the cache: only if it has changed we should update the 189 // view. 190 NumberWithCountryIso numberCountryIso = 191 new NumberWithCountryIso(request.number, request.countryIso); 192 ContactInfo existingInfo = mCache.getPossiblyExpired(numberCountryIso); 193 194 final boolean isRemoteSource = info.sourceType != Type.UNKNOWN_SOURCE_TYPE; 195 196 // Don't force redraw if existing info in the cache is equal to {@link ContactInfo#EMPTY} 197 // to avoid updating the data set for every new row that is scrolled into view. 198 199 // Exception: Photo uris for contacts from remote sources are not cached in the call log 200 // cache, so we have to force a redraw for these contacts regardless. 201 boolean updated = 202 (!Objects.equals(existingInfo, ContactInfo.EMPTY) || isRemoteSource) 203 && !info.equals(existingInfo); 204 205 // Store the data in the cache so that the UI thread can use to display it. Store it 206 // even if it has not changed so that it is marked as not expired. 207 mCache.put(numberCountryIso, info); 208 209 // Update the call log even if the cache it is up-to-date: it is possible that the cache 210 // contains the value from a different call log entry. 211 mContactInfoHelper.updateCallLogContactInfo( 212 request.number, request.countryIso, info, request.callLogInfo); 213 if (!request.isLocalRequest()) { 214 mContactInfoHelper.updateCachedNumberLookupService(info); 215 } 216 return updated; 217 } 218 219 /** 220 * After a delay, start the thread to begin processing requests. We perform lookups on a 221 * background thread, but this must be called to indicate the thread should be running. 222 */ start()223 public void start() { 224 // Schedule a thread-creation message if the thread hasn't been created yet, as an 225 // optimization to queue fewer messages. 226 if (mContactInfoQueryThread == null) { 227 // TODO: Check whether this delay before starting to process is necessary. 228 mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MS); 229 } 230 } 231 232 /** 233 * Stops the thread and clears the queue of messages to process. This cleans up the thread for 234 * lookups so that it is not perpetually running. 235 */ stop()236 public void stop() { 237 stopRequestProcessing(); 238 } 239 240 /** 241 * Starts a background thread to process contact-lookup requests, unless one has already been 242 * started. 243 */ startRequestProcessing()244 private synchronized void startRequestProcessing() { 245 // For unit-testing. 246 if (mRequestProcessingDisabled) { 247 return; 248 } 249 250 // If a thread is already started, don't start another. 251 if (mContactInfoQueryThread != null) { 252 return; 253 } 254 255 mContactInfoQueryThread = new QueryThread(); 256 mContactInfoQueryThread.setPriority(Thread.MIN_PRIORITY); 257 mContactInfoQueryThread.start(); 258 } 259 invalidate()260 public void invalidate() { 261 mCache.expireAll(); 262 stopRequestProcessing(); 263 } 264 265 /** 266 * Stops the background thread that processes updates and cancels any pending requests to start 267 * it. 268 */ stopRequestProcessing()269 private synchronized void stopRequestProcessing() { 270 // Remove any pending requests to start the processing thread. 271 mHandler.removeMessages(START_THREAD); 272 if (mContactInfoQueryThread != null) { 273 // Stop the thread; we are finished with it. 274 mContactInfoQueryThread.stopProcessing(); 275 mContactInfoQueryThread.interrupt(); 276 mContactInfoQueryThread = null; 277 } 278 } 279 280 /** 281 * Enqueues a request to look up the contact details for the given phone number. 282 * 283 * <p>It also provides the current contact info stored in the call log for this number. 284 * 285 * <p>If the {@code immediate} parameter is true, it will start immediately the thread that looks 286 * up the contact information (if it has not been already started). Otherwise, it will be started 287 * with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MS}. 288 */ enqueueRequest( String number, String countryIso, ContactInfo callLogInfo, boolean immediate, @ContactInfoRequest.TYPE int type)289 private void enqueueRequest( 290 String number, 291 String countryIso, 292 ContactInfo callLogInfo, 293 boolean immediate, 294 @ContactInfoRequest.TYPE int type) { 295 ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo, type); 296 if (!mUpdateRequests.contains(request)) { 297 mUpdateRequests.offer(request); 298 } 299 300 if (immediate) { 301 startRequestProcessing(); 302 } 303 } 304 305 /** Checks whether the contact info from the call log matches the one from the contacts db. */ callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info)306 private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) { 307 // The call log only contains a subset of the fields in the contacts db. Only check those. 308 return TextUtils.equals(callLogInfo.name, info.name) 309 && callLogInfo.type == info.type 310 && TextUtils.equals(callLogInfo.label, info.label); 311 } 312 313 /** Sets whether processing of requests for contact details should be enabled. */ disableRequestProcessing()314 public void disableRequestProcessing() { 315 mRequestProcessingDisabled = true; 316 } 317 318 @VisibleForTesting injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo)319 public void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) { 320 NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); 321 mCache.put(numberCountryIso, contactInfo); 322 } 323 324 public interface OnContactInfoChangedListener { 325 onContactInfoChanged()326 void onContactInfoChanged(); 327 } 328 329 /* 330 * Handles requests for contact name and number type. 331 */ 332 private class QueryThread extends Thread { 333 334 private volatile boolean mDone = false; 335 QueryThread()336 public QueryThread() { 337 super("ContactInfoCache.QueryThread"); 338 } 339 stopProcessing()340 public void stopProcessing() { 341 mDone = true; 342 } 343 344 @Override run()345 public void run() { 346 boolean shouldRedraw = false; 347 while (true) { 348 // Check if thread is finished, and if so return immediately. 349 if (mDone) { 350 return; 351 } 352 353 try { 354 ContactInfoRequest request = mUpdateRequests.take(); 355 shouldRedraw |= queryContactInfo(request); 356 if (shouldRedraw 357 && (mUpdateRequests.isEmpty() 358 || (request.isLocalRequest() && !mUpdateRequests.peek().isLocalRequest()))) { 359 shouldRedraw = false; 360 mHandler.sendEmptyMessage(REDRAW); 361 } 362 } catch (InterruptedException e) { 363 // Ignore and attempt to continue processing requests 364 } 365 } 366 } 367 } 368 } 369