Home | History | Annotate | Download | only in contactinfo
      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 
     65     public InnerHandler(WeakReference<ContactInfoCache> contactInfoCacheWeakReference) {
     66       this.contactInfoCacheWeakReference = contactInfoCacheWeakReference;
     67     }
     68 
     69     @Override
     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 
     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 
     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    */
    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    */
    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    */
    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    */
    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 
    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    */
    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    */
    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. */
    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. */
    314   public void disableRequestProcessing() {
    315     mRequestProcessingDisabled = true;
    316   }
    317 
    318   @VisibleForTesting
    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 
    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 
    336     public QueryThread() {
    337       super("ContactInfoCache.QueryThread");
    338     }
    339 
    340     public void stopProcessing() {
    341       mDone = true;
    342     }
    343 
    344     @Override
    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