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.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