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