• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
onContactInfoChanged()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 
QueryThread()53         public QueryThread() {
54             super("ContactInfoCache.QueryThread");
55         }
56 
stopProcessing()57         public void stopProcessing() {
58             mDone = true;
59         }
60 
61         @Override
run()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 
ContactInfoCache(ContactInfoHelper contactInfoHelper, OnContactInfoChangedListener onContactInfoChangedListener)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 
getValue(String number, String countryIso, ContactInfo cachedContactInfo)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      */
queryContactInfo(String number, String countryIso, ContactInfo callLogInfo)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      */
start()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      */
stop()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      */
startRequestProcessing()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 
invalidate()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      */
stopRequestProcessing()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      */
enqueueRequest(String number, String countryIso, ContactInfo callLogInfo, boolean immediate)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      */
callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info)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      */
disableRequestProcessing()324     public void disableRequestProcessing() {
325         mRequestProcessingDisabled = true;
326     }
327 
328     @VisibleForTesting
injectContactInfoForTest( String number, String countryIso, ContactInfo contactInfo)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