• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 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.incallui;
18 
19 import android.content.Context;
20 import android.graphics.Bitmap;
21 import android.graphics.drawable.BitmapDrawable;
22 import android.graphics.drawable.Drawable;
23 import android.net.Uri;
24 import android.os.Looper;
25 import android.provider.ContactsContract.Contacts;
26 import android.provider.ContactsContract.CommonDataKinds.Phone;
27 import android.telecom.TelecomManager;
28 import android.text.TextUtils;
29 
30 import com.android.contacts.common.util.PhoneNumberHelper;
31 import com.android.incallui.service.PhoneNumberService;
32 import com.android.incalluibind.ServiceFactory;
33 import com.android.services.telephony.common.MoreStrings;
34 import com.google.common.collect.Maps;
35 import com.google.common.collect.Sets;
36 import com.google.common.base.Objects;
37 import com.google.common.base.Preconditions;
38 
39 import java.util.HashMap;
40 import java.util.Set;
41 
42 /**
43  * Class responsible for querying Contact Information for Call objects. Can perform asynchronous
44  * requests to the Contact Provider for information as well as respond synchronously for any data
45  * that it currently has cached from previous queries. This class always gets called from the UI
46  * thread so it does not need thread protection.
47  */
48 public class ContactInfoCache implements ContactsAsyncHelper.OnImageLoadCompleteListener {
49 
50     private static final String TAG = ContactInfoCache.class.getSimpleName();
51     private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0;
52 
53     private final Context mContext;
54     private final PhoneNumberService mPhoneNumberService;
55     private final HashMap<String, ContactCacheEntry> mInfoMap = Maps.newHashMap();
56     private final HashMap<String, Set<ContactInfoCacheCallback>> mCallBacks = Maps.newHashMap();
57 
58     private static ContactInfoCache sCache = null;
59 
getInstance(Context mContext)60     public static synchronized ContactInfoCache getInstance(Context mContext) {
61         if (sCache == null) {
62             sCache = new ContactInfoCache(mContext.getApplicationContext());
63         }
64         return sCache;
65     }
66 
ContactInfoCache(Context context)67     private ContactInfoCache(Context context) {
68         mContext = context;
69         mPhoneNumberService = ServiceFactory.newPhoneNumberService(context);
70     }
71 
getInfo(String callId)72     public ContactCacheEntry getInfo(String callId) {
73         return mInfoMap.get(callId);
74     }
75 
buildCacheEntryFromCall(Context context, Call call, boolean isIncoming)76     public static ContactCacheEntry buildCacheEntryFromCall(Context context, Call call,
77             boolean isIncoming) {
78         final ContactCacheEntry entry = new ContactCacheEntry();
79 
80         // TODO: get rid of caller info.
81         final CallerInfo info = CallerInfoUtils.buildCallerInfo(context, call);
82         ContactInfoCache.populateCacheEntry(context, info, entry, call.getNumberPresentation(),
83                 isIncoming);
84         return entry;
85     }
86 
87     private class FindInfoCallback implements CallerInfoAsyncQuery.OnQueryCompleteListener {
88         private final boolean mIsIncoming;
89 
FindInfoCallback(boolean isIncoming)90         public FindInfoCallback(boolean isIncoming) {
91             mIsIncoming = isIncoming;
92         }
93 
94         @Override
onQueryComplete(int token, Object cookie, CallerInfo callerInfo)95         public void onQueryComplete(int token, Object cookie, CallerInfo callerInfo) {
96             findInfoQueryComplete((Call) cookie, callerInfo, mIsIncoming, true);
97         }
98     }
99 
100     /**
101      * Requests contact data for the Call object passed in.
102      * Returns the data through callback.  If callback is null, no response is made, however the
103      * query is still performed and cached.
104      *
105      * @param callback The function to call back when the call is found. Can be null.
106      */
findInfo(final Call call, final boolean isIncoming, ContactInfoCacheCallback callback)107     public void findInfo(final Call call, final boolean isIncoming,
108             ContactInfoCacheCallback callback) {
109         Preconditions.checkState(Looper.getMainLooper().getThread() == Thread.currentThread());
110         Preconditions.checkNotNull(callback);
111 
112         final String callId = call.getId();
113         final ContactCacheEntry cacheEntry = mInfoMap.get(callId);
114         Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
115 
116         // If we have a previously obtained intermediate result return that now
117         if (cacheEntry != null) {
118             Log.d(TAG, "Contact lookup. In memory cache hit; lookup "
119                     + (callBacks == null ? "complete" : "still running"));
120             callback.onContactInfoComplete(callId, cacheEntry);
121             // If no other callbacks are in flight, we're done.
122             if (callBacks == null) {
123                 return;
124             }
125         }
126 
127         // If the entry already exists, add callback
128         if (callBacks != null) {
129             callBacks.add(callback);
130             return;
131         }
132         Log.d(TAG, "Contact lookup. In memory cache miss; searching provider.");
133         // New lookup
134         callBacks = Sets.newHashSet();
135         callBacks.add(callback);
136         mCallBacks.put(callId, callBacks);
137 
138         /**
139          * Performs a query for caller information.
140          * Save any immediate data we get from the query. An asynchronous query may also be made
141          * for any data that we do not already have. Some queries, such as those for voicemail and
142          * emergency call information, will not perform an additional asynchronous query.
143          */
144         final CallerInfo callerInfo = CallerInfoUtils.getCallerInfoForCall(
145                 mContext, call, new FindInfoCallback(isIncoming));
146 
147         findInfoQueryComplete(call, callerInfo, isIncoming, false);
148     }
149 
findInfoQueryComplete(Call call, CallerInfo callerInfo, boolean isIncoming, boolean didLocalLookup)150     private void findInfoQueryComplete(Call call, CallerInfo callerInfo, boolean isIncoming,
151             boolean didLocalLookup) {
152         final String callId = call.getId();
153         int presentationMode = call.getNumberPresentation();
154         if (callerInfo.contactExists || callerInfo.isEmergencyNumber() ||
155                 callerInfo.isVoiceMailNumber()) {
156             presentationMode = TelecomManager.PRESENTATION_ALLOWED;
157         }
158 
159         ContactCacheEntry cacheEntry = mInfoMap.get(callId);
160         // Rebuild the entry from the new data if:
161         // 1) This is NOT the asynchronous local lookup (IOW, this is the first pass)
162         // 2) The local lookup was done and the contact exists
163         // 3) The existing cached entry is empty (no name).
164         if (!didLocalLookup || callerInfo.contactExists ||
165                 (cacheEntry != null && TextUtils.isEmpty(cacheEntry.name))) {
166             cacheEntry = buildEntry(mContext, callId, callerInfo, presentationMode, isIncoming);
167             mInfoMap.put(callId, cacheEntry);
168         }
169 
170         sendInfoNotifications(callId, cacheEntry);
171 
172         if (didLocalLookup) {
173             // Before issuing a request for more data from other services, We only check that the
174             // contact wasn't found in the local DB.  We don't check the if the cache entry already
175             // has a name because we allow overriding cnap data with data from other services.
176             if (!callerInfo.contactExists && mPhoneNumberService != null) {
177                 Log.d(TAG, "Contact lookup. Local contacts miss, checking remote");
178                 final PhoneNumberServiceListener listener = new PhoneNumberServiceListener(callId);
179                 mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener,
180                         isIncoming);
181             } else if (cacheEntry.displayPhotoUri != null) {
182                 Log.d(TAG, "Contact lookup. Local contact found, starting image load");
183                 // Load the image with a callback to update the image state.
184                 // When the load is finished, onImageLoadComplete() will be called.
185                 ContactsAsyncHelper.startObtainPhotoAsync(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
186                         mContext, cacheEntry.displayPhotoUri, ContactInfoCache.this, callId);
187             } else {
188                 if (callerInfo.contactExists) {
189                     Log.d(TAG, "Contact lookup done. Local contact found, no image.");
190                 } else {
191                     Log.d(TAG, "Contact lookup done. Local contact not found and"
192                             + " no remote lookup service available.");
193                 }
194                 clearCallbacks(callId);
195             }
196         }
197     }
198 
199     class PhoneNumberServiceListener implements PhoneNumberService.NumberLookupListener,
200                                      PhoneNumberService.ImageLookupListener {
201         private final String mCallId;
202 
PhoneNumberServiceListener(String callId)203         PhoneNumberServiceListener(String callId) {
204             mCallId = callId;
205         }
206 
207         @Override
onPhoneNumberInfoComplete( final PhoneNumberService.PhoneNumberInfo info)208         public void onPhoneNumberInfoComplete(
209                 final PhoneNumberService.PhoneNumberInfo info) {
210             // If we got a miss, this is the end of the lookup pipeline,
211             // so clear the callbacks and return.
212             if (info == null) {
213                 Log.d(TAG, "Contact lookup done. Remote contact not found.");
214                 clearCallbacks(mCallId);
215                 return;
216             }
217 
218             ContactCacheEntry entry = new ContactCacheEntry();
219             entry.name = info.getDisplayName();
220             entry.number = info.getNumber();
221             final int type = info.getPhoneType();
222             final String label = info.getPhoneLabel();
223             if (type == Phone.TYPE_CUSTOM) {
224                 entry.label = label;
225             } else {
226                 final CharSequence typeStr = Phone.getTypeLabel(
227                         mContext.getResources(), type, label);
228                 entry.label = typeStr == null ? null : typeStr.toString();
229             }
230             final ContactCacheEntry oldEntry = mInfoMap.get(mCallId);
231             if (oldEntry != null) {
232                 // Location is only obtained from local lookup so persist
233                 // the value for remote lookups. Once we have a name this
234                 // field is no longer used; it is persisted here in case
235                 // the UI is ever changed to use it.
236                 entry.location = oldEntry.location;
237             }
238 
239             // If no image and it's a business, switch to using the default business avatar.
240             if (info.getImageUrl() == null && info.isBusiness()) {
241                 Log.d(TAG, "Business has no image. Using default.");
242                 entry.photo = mContext.getResources().getDrawable(R.drawable.img_business);
243             }
244 
245             // Add the contact info to the cache.
246             mInfoMap.put(mCallId, entry);
247             sendInfoNotifications(mCallId, entry);
248 
249             // If there is no image then we should not expect another callback.
250             if (info.getImageUrl() == null) {
251                 // We're done, so clear callbacks
252                 clearCallbacks(mCallId);
253             }
254         }
255 
256         @Override
onImageFetchComplete(Bitmap bitmap)257         public void onImageFetchComplete(Bitmap bitmap) {
258             onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, mCallId);
259         }
260     }
261 
262     /**
263      * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface.
264      * make sure that the call state is reflected after the image is loaded.
265      */
266     @Override
onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie)267     public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
268         Log.d(this, "Image load complete with context: ", mContext);
269         // TODO: may be nice to update the image view again once the newer one
270         // is available on contacts database.
271 
272         final String callId = (String) cookie;
273         final ContactCacheEntry entry = mInfoMap.get(callId);
274 
275         if (entry == null) {
276             Log.e(this, "Image Load received for empty search entry.");
277             clearCallbacks(callId);
278             return;
279         }
280         Log.d(this, "setting photo for entry: ", entry);
281 
282         // Conference call icons are being handled in CallCardPresenter.
283         if (photo != null) {
284             Log.v(this, "direct drawable: ", photo);
285             entry.photo = photo;
286         } else if (photoIcon != null) {
287             Log.v(this, "photo icon: ", photoIcon);
288             entry.photo = new BitmapDrawable(mContext.getResources(), photoIcon);
289         } else {
290             Log.v(this, "unknown photo");
291             entry.photo = null;
292         }
293 
294         sendImageNotifications(callId, entry);
295         clearCallbacks(callId);
296     }
297 
298     /**
299      * Blows away the stored cache values.
300      */
clearCache()301     public void clearCache() {
302         mInfoMap.clear();
303         mCallBacks.clear();
304     }
305 
buildEntry(Context context, String callId, CallerInfo info, int presentation, boolean isIncoming)306     private ContactCacheEntry buildEntry(Context context, String callId,
307             CallerInfo info, int presentation, boolean isIncoming) {
308         // The actual strings we're going to display onscreen:
309         Drawable photo = null;
310 
311         final ContactCacheEntry cce = new ContactCacheEntry();
312         populateCacheEntry(context, info, cce, presentation, isIncoming);
313 
314         // This will only be true for emergency numbers
315         if (info.photoResource != 0) {
316             photo = context.getResources().getDrawable(info.photoResource);
317         } else if (info.isCachedPhotoCurrent) {
318             if (info.cachedPhoto != null) {
319                 photo = info.cachedPhoto;
320             } else {
321                 photo = context.getResources().getDrawable(R.drawable.img_no_image);
322                 photo.setAutoMirrored(true);
323             }
324         } else if (info.contactDisplayPhotoUri == null) {
325             photo = context.getResources().getDrawable(R.drawable.img_no_image);
326             photo.setAutoMirrored(true);
327         } else {
328             cce.displayPhotoUri = info.contactDisplayPhotoUri;
329         }
330 
331         if (info.lookupKeyOrNull == null || info.contactIdOrZero == 0) {
332             Log.v(TAG, "lookup key is null or contact ID is 0. Don't create a lookup uri.");
333             cce.lookupUri = null;
334         } else {
335             cce.lookupUri = Contacts.getLookupUri(info.contactIdOrZero, info.lookupKeyOrNull);
336         }
337 
338         cce.photo = photo;
339         cce.lookupKey = info.lookupKeyOrNull;
340 
341         return cce;
342     }
343 
344     /**
345      * Populate a cache entry from a call (which got converted into a caller info).
346      */
populateCacheEntry(Context context, CallerInfo info, ContactCacheEntry cce, int presentation, boolean isIncoming)347     public static void populateCacheEntry(Context context, CallerInfo info, ContactCacheEntry cce,
348             int presentation, boolean isIncoming) {
349         Preconditions.checkNotNull(info);
350         String displayName = null;
351         String displayNumber = null;
352         String displayLocation = null;
353         String label = null;
354         boolean isSipCall = false;
355 
356             // It appears that there is a small change in behaviour with the
357             // PhoneUtils' startGetCallerInfo whereby if we query with an
358             // empty number, we will get a valid CallerInfo object, but with
359             // fields that are all null, and the isTemporary boolean input
360             // parameter as true.
361 
362             // In the past, we would see a NULL callerinfo object, but this
363             // ends up causing null pointer exceptions elsewhere down the
364             // line in other cases, so we need to make this fix instead. It
365             // appears that this was the ONLY call to PhoneUtils
366             // .getCallerInfo() that relied on a NULL CallerInfo to indicate
367             // an unknown contact.
368 
369             // Currently, infi.phoneNumber may actually be a SIP address, and
370             // if so, it might sometimes include the "sip:" prefix. That
371             // prefix isn't really useful to the user, though, so strip it off
372             // if present. (For any other URI scheme, though, leave the
373             // prefix alone.)
374             // TODO: It would be cleaner for CallerInfo to explicitly support
375             // SIP addresses instead of overloading the "phoneNumber" field.
376             // Then we could remove this hack, and instead ask the CallerInfo
377             // for a "user visible" form of the SIP address.
378             String number = info.phoneNumber;
379 
380             if (!TextUtils.isEmpty(number)) {
381                 isSipCall = PhoneNumberHelper.isUriNumber(number);
382                 if (number.startsWith("sip:")) {
383                     number = number.substring(4);
384                 }
385             }
386 
387             if (TextUtils.isEmpty(info.name)) {
388                 // No valid "name" in the CallerInfo, so fall back to
389                 // something else.
390                 // (Typically, we promote the phone number up to the "name" slot
391                 // onscreen, and possibly display a descriptive string in the
392                 // "number" slot.)
393                 if (TextUtils.isEmpty(number)) {
394                     // No name *or* number! Display a generic "unknown" string
395                     // (or potentially some other default based on the presentation.)
396                     displayName = getPresentationString(context, presentation);
397                     Log.d(TAG, "  ==> no name *or* number! displayName = " + displayName);
398                 } else if (presentation != TelecomManager.PRESENTATION_ALLOWED) {
399                     // This case should never happen since the network should never send a phone #
400                     // AND a restricted presentation. However we leave it here in case of weird
401                     // network behavior
402                     displayName = getPresentationString(context, presentation);
403                     Log.d(TAG, "  ==> presentation not allowed! displayName = " + displayName);
404                 } else if (!TextUtils.isEmpty(info.cnapName)) {
405                     // No name, but we do have a valid CNAP name, so use that.
406                     displayName = info.cnapName;
407                     info.name = info.cnapName;
408                     displayNumber = number;
409                     Log.d(TAG, "  ==> cnapName available: displayName '" + displayName +
410                             "', displayNumber '" + displayNumber + "'");
411                 } else {
412                     // No name; all we have is a number. This is the typical
413                     // case when an incoming call doesn't match any contact,
414                     // or if you manually dial an outgoing number using the
415                     // dialpad.
416                     displayNumber = number;
417 
418                     // Display a geographical description string if available
419                     // (but only for incoming calls.)
420                     if (isIncoming) {
421                         // TODO (CallerInfoAsyncQuery cleanup): Fix the CallerInfo
422                         // query to only do the geoDescription lookup in the first
423                         // place for incoming calls.
424                         displayLocation = info.geoDescription; // may be null
425                         Log.d(TAG, "Geodescrption: " + info.geoDescription);
426                     }
427 
428                     Log.d(TAG, "  ==>  no name; falling back to number:"
429                             + " displayNumber '" + Log.pii(displayNumber)
430                             + "', displayLocation '" + displayLocation + "'");
431                 }
432             } else {
433                 // We do have a valid "name" in the CallerInfo. Display that
434                 // in the "name" slot, and the phone number in the "number" slot.
435                 if (presentation != TelecomManager.PRESENTATION_ALLOWED) {
436                     // This case should never happen since the network should never send a name
437                     // AND a restricted presentation. However we leave it here in case of weird
438                     // network behavior
439                     displayName = getPresentationString(context, presentation);
440                     Log.d(TAG, "  ==> valid name, but presentation not allowed!" +
441                             " displayName = " + displayName);
442                 } else {
443                     displayName = info.name;
444                     displayNumber = number;
445                     label = info.phoneLabel;
446                     Log.d(TAG, "  ==>  name is present in CallerInfo: displayName '" + displayName
447                             + "', displayNumber '" + displayNumber + "'");
448                 }
449             }
450 
451         cce.name = displayName;
452         cce.number = displayNumber;
453         cce.location = displayLocation;
454         cce.label = label;
455         cce.isSipCall = isSipCall;
456     }
457 
458     /**
459      * Sends the updated information to call the callbacks for the entry.
460      */
sendInfoNotifications(String callId, ContactCacheEntry entry)461     private void sendInfoNotifications(String callId, ContactCacheEntry entry) {
462         final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
463         if (callBacks != null) {
464             for (ContactInfoCacheCallback callBack : callBacks) {
465                 callBack.onContactInfoComplete(callId, entry);
466             }
467         }
468     }
469 
sendImageNotifications(String callId, ContactCacheEntry entry)470     private void sendImageNotifications(String callId, ContactCacheEntry entry) {
471         final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
472         if (callBacks != null && entry.photo != null) {
473             for (ContactInfoCacheCallback callBack : callBacks) {
474                 callBack.onImageLoadComplete(callId, entry);
475             }
476         }
477     }
478 
clearCallbacks(String callId)479     private void clearCallbacks(String callId) {
480         mCallBacks.remove(callId);
481     }
482 
483     /**
484      * Gets name strings based on some special presentation modes.
485      */
getPresentationString(Context context, int presentation)486     private static String getPresentationString(Context context, int presentation) {
487         String name = context.getString(R.string.unknown);
488         if (presentation == TelecomManager.PRESENTATION_RESTRICTED) {
489             name = context.getString(R.string.private_num);
490         } else if (presentation == TelecomManager.PRESENTATION_PAYPHONE) {
491             name = context.getString(R.string.payphone);
492         }
493         return name;
494     }
495 
496     /**
497      * Callback interface for the contact query.
498      */
499     public interface ContactInfoCacheCallback {
onContactInfoComplete(String callId, ContactCacheEntry entry)500         public void onContactInfoComplete(String callId, ContactCacheEntry entry);
onImageLoadComplete(String callId, ContactCacheEntry entry)501         public void onImageLoadComplete(String callId, ContactCacheEntry entry);
502     }
503 
504     public static class ContactCacheEntry {
505         public String name;
506         public String number;
507         public String location;
508         public String label;
509         public Drawable photo;
510         public boolean isSipCall;
511         /** This will be used for the "view" notification. */
512         public Uri contactUri;
513         /** Either a display photo or a thumbnail URI. */
514         public Uri displayPhotoUri;
515         public Uri lookupUri; // Sent to NotificationMananger
516         public String lookupKey;
517 
518         @Override
toString()519         public String toString() {
520             return Objects.toStringHelper(this)
521                     .add("name", MoreStrings.toSafeString(name))
522                     .add("number", MoreStrings.toSafeString(number))
523                     .add("location", MoreStrings.toSafeString(location))
524                     .add("label", label)
525                     .add("photo", photo)
526                     .add("isSipCall", isSipCall)
527                     .add("contactUri", contactUri)
528                     .add("displayPhotoUri", displayPhotoUri)
529                     .toString();
530         }
531     }
532 }
533