blob: 4c8963a982d76919b8f974906f8f7d00231fddc3 [file] [log] [blame]
Eric Erfanianccca3152017-02-22 16:32:36 -08001/*
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
17package com.android.incallui;
18
19import android.content.Context;
20import android.graphics.Bitmap;
21import android.graphics.drawable.BitmapDrawable;
22import android.graphics.drawable.Drawable;
23import android.media.RingtoneManager;
24import android.net.Uri;
25import android.os.AsyncTask;
26import android.os.Build.VERSION;
27import android.os.Build.VERSION_CODES;
Eric Erfanian9779f962017-03-27 12:31:48 -070028import android.os.SystemClock;
Eric Erfanianccca3152017-02-22 16:32:36 -080029import android.provider.ContactsContract;
30import android.provider.ContactsContract.CommonDataKinds.Phone;
31import android.provider.ContactsContract.Contacts;
32import android.provider.ContactsContract.DisplayNameSources;
33import android.support.annotation.AnyThread;
34import android.support.annotation.MainThread;
35import android.support.annotation.NonNull;
36import android.support.annotation.WorkerThread;
37import android.support.v4.os.UserManagerCompat;
38import android.telecom.TelecomManager;
Eric Erfaniand5e47f62017-03-15 14:41:07 -070039import android.telephony.PhoneNumberUtils;
Eric Erfanianccca3152017-02-22 16:32:36 -080040import android.text.TextUtils;
41import android.util.ArrayMap;
42import android.util.ArraySet;
43import com.android.contacts.common.ContactsUtils;
44import com.android.dialer.common.Assert;
45import com.android.dialer.logging.nano.ContactLookupResult;
Eric Erfanian9779f962017-03-27 12:31:48 -070046import com.android.dialer.oem.CequintCallerIdManager;
47import com.android.dialer.oem.CequintCallerIdManager.CequintCallerIdContact;
Eric Erfanianccca3152017-02-22 16:32:36 -080048import com.android.dialer.phonenumbercache.CachedNumberLookupService;
49import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
50import com.android.dialer.phonenumbercache.ContactInfo;
51import com.android.dialer.phonenumbercache.PhoneNumberCache;
52import com.android.dialer.phonenumberutil.PhoneNumberHelper;
53import com.android.dialer.util.MoreStrings;
54import com.android.incallui.CallerInfoAsyncQuery.OnQueryCompleteListener;
55import com.android.incallui.ContactsAsyncHelper.OnImageLoadCompleteListener;
56import com.android.incallui.bindings.PhoneNumberService;
57import com.android.incallui.call.DialerCall;
58import com.android.incallui.incall.protocol.ContactPhotoType;
59import java.util.Map;
60import java.util.Objects;
61import java.util.Set;
62import java.util.concurrent.ConcurrentHashMap;
63import org.json.JSONException;
64import org.json.JSONObject;
65
66/**
67 * Class responsible for querying Contact Information for DialerCall objects. Can perform
68 * asynchronous requests to the Contact Provider for information as well as respond synchronously
69 * for any data that it currently has cached from previous queries. This class always gets called
70 * from the UI thread so it does not need thread protection.
71 */
72public class ContactInfoCache implements OnImageLoadCompleteListener {
73
74 private static final String TAG = ContactInfoCache.class.getSimpleName();
75 private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0;
76 private static ContactInfoCache sCache = null;
77 private final Context mContext;
78 private final PhoneNumberService mPhoneNumberService;
79 // Cache info map needs to be thread-safe since it could be modified by both main thread and
80 // worker thread.
Eric Erfaniand5e47f62017-03-15 14:41:07 -070081 private final ConcurrentHashMap<String, ContactCacheEntry> mInfoMap = new ConcurrentHashMap<>();
Eric Erfanianccca3152017-02-22 16:32:36 -080082 private final Map<String, Set<ContactInfoCacheCallback>> mCallBacks = new ArrayMap<>();
83 private Drawable mDefaultContactPhotoDrawable;
84 private Drawable mConferencePhotoDrawable;
Eric Erfaniand5e47f62017-03-15 14:41:07 -070085 private int mQueryId;
Eric Erfanianccca3152017-02-22 16:32:36 -080086
87 private ContactInfoCache(Context context) {
88 mContext = context;
89 mPhoneNumberService = Bindings.get(context).newPhoneNumberService(context);
90 }
91
92 public static synchronized ContactInfoCache getInstance(Context mContext) {
93 if (sCache == null) {
94 sCache = new ContactInfoCache(mContext.getApplicationContext());
95 }
96 return sCache;
97 }
98
Eric Erfaniand5e47f62017-03-15 14:41:07 -070099 static ContactCacheEntry buildCacheEntryFromCall(
Eric Erfanianccca3152017-02-22 16:32:36 -0800100 Context context, DialerCall call, boolean isIncoming) {
101 final ContactCacheEntry entry = new ContactCacheEntry();
102
103 // TODO: get rid of caller info.
104 final CallerInfo info = CallerInfoUtils.buildCallerInfo(context, call);
105 ContactInfoCache.populateCacheEntry(
106 context, info, entry, call.getNumberPresentation(), isIncoming);
107 return entry;
108 }
109
110 /** Populate a cache entry from a call (which got converted into a caller info). */
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700111 private static void populateCacheEntry(
Eric Erfanianccca3152017-02-22 16:32:36 -0800112 @NonNull Context context,
113 @NonNull CallerInfo info,
114 @NonNull ContactCacheEntry cce,
115 int presentation,
116 boolean isIncoming) {
117 Objects.requireNonNull(info);
118 String displayName = null;
119 String displayNumber = null;
120 String displayLocation = null;
121 String label = null;
122 boolean isSipCall = false;
123
124 // It appears that there is a small change in behaviour with the
125 // PhoneUtils' startGetCallerInfo whereby if we query with an
126 // empty number, we will get a valid CallerInfo object, but with
127 // fields that are all null, and the isTemporary boolean input
128 // parameter as true.
129
130 // In the past, we would see a NULL callerinfo object, but this
131 // ends up causing null pointer exceptions elsewhere down the
132 // line in other cases, so we need to make this fix instead. It
133 // appears that this was the ONLY call to PhoneUtils
134 // .getCallerInfo() that relied on a NULL CallerInfo to indicate
135 // an unknown contact.
136
137 // Currently, info.phoneNumber may actually be a SIP address, and
138 // if so, it might sometimes include the "sip:" prefix. That
139 // prefix isn't really useful to the user, though, so strip it off
140 // if present. (For any other URI scheme, though, leave the
141 // prefix alone.)
142 // TODO: It would be cleaner for CallerInfo to explicitly support
143 // SIP addresses instead of overloading the "phoneNumber" field.
144 // Then we could remove this hack, and instead ask the CallerInfo
145 // for a "user visible" form of the SIP address.
146 String number = info.phoneNumber;
147
148 if (!TextUtils.isEmpty(number)) {
149 isSipCall = PhoneNumberHelper.isUriNumber(number);
150 if (number.startsWith("sip:")) {
151 number = number.substring(4);
152 }
153 }
154
155 if (TextUtils.isEmpty(info.name)) {
156 // No valid "name" in the CallerInfo, so fall back to
157 // something else.
158 // (Typically, we promote the phone number up to the "name" slot
159 // onscreen, and possibly display a descriptive string in the
160 // "number" slot.)
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700161 if (TextUtils.isEmpty(number) && TextUtils.isEmpty(info.cnapName)) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800162 // No name *or* number! Display a generic "unknown" string
163 // (or potentially some other default based on the presentation.)
164 displayName = getPresentationString(context, presentation, info.callSubject);
165 Log.d(TAG, " ==> no name *or* number! displayName = " + displayName);
166 } else if (presentation != TelecomManager.PRESENTATION_ALLOWED) {
167 // This case should never happen since the network should never send a phone #
168 // AND a restricted presentation. However we leave it here in case of weird
169 // network behavior
170 displayName = getPresentationString(context, presentation, info.callSubject);
171 Log.d(TAG, " ==> presentation not allowed! displayName = " + displayName);
172 } else if (!TextUtils.isEmpty(info.cnapName)) {
173 // No name, but we do have a valid CNAP name, so use that.
174 displayName = info.cnapName;
175 info.name = info.cnapName;
176 displayNumber = PhoneNumberHelper.formatNumber(number, context);
177 Log.d(
178 TAG,
179 " ==> cnapName available: displayName '"
180 + displayName
181 + "', displayNumber '"
182 + displayNumber
183 + "'");
184 } else {
185 // No name; all we have is a number. This is the typical
186 // case when an incoming call doesn't match any contact,
187 // or if you manually dial an outgoing number using the
188 // dialpad.
189 displayNumber = PhoneNumberHelper.formatNumber(number, context);
190
191 // Display a geographical description string if available
192 // (but only for incoming calls.)
193 if (isIncoming) {
194 // TODO (CallerInfoAsyncQuery cleanup): Fix the CallerInfo
195 // query to only do the geoDescription lookup in the first
196 // place for incoming calls.
197 displayLocation = info.geoDescription; // may be null
198 Log.d(TAG, "Geodescrption: " + info.geoDescription);
199 }
200
201 Log.d(
202 TAG,
203 " ==> no name; falling back to number:"
204 + " displayNumber '"
205 + Log.pii(displayNumber)
206 + "', displayLocation '"
207 + displayLocation
208 + "'");
209 }
210 } else {
211 // We do have a valid "name" in the CallerInfo. Display that
212 // in the "name" slot, and the phone number in the "number" slot.
213 if (presentation != TelecomManager.PRESENTATION_ALLOWED) {
214 // This case should never happen since the network should never send a name
215 // AND a restricted presentation. However we leave it here in case of weird
216 // network behavior
217 displayName = getPresentationString(context, presentation, info.callSubject);
218 Log.d(
219 TAG,
220 " ==> valid name, but presentation not allowed!" + " displayName = " + displayName);
221 } else {
222 // Causes cce.namePrimary to be set as info.name below. CallCardPresenter will
223 // later determine whether to use the name or nameAlternative when presenting
224 displayName = info.name;
225 cce.nameAlternative = info.nameAlternative;
226 displayNumber = PhoneNumberHelper.formatNumber(number, context);
227 label = info.phoneLabel;
228 Log.d(
229 TAG,
230 " ==> name is present in CallerInfo: displayName '"
231 + displayName
232 + "', displayNumber '"
233 + displayNumber
234 + "'");
235 }
236 }
237
238 cce.namePrimary = displayName;
239 cce.number = displayNumber;
240 cce.location = displayLocation;
241 cce.label = label;
242 cce.isSipCall = isSipCall;
243 cce.userType = info.userType;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700244 cce.originalPhoneNumber = info.phoneNumber;
Eric Erfanianccca3152017-02-22 16:32:36 -0800245
246 if (info.contactExists) {
247 cce.contactLookupResult = ContactLookupResult.Type.LOCAL_CONTACT;
248 }
249 }
250
251 /** Gets name strings based on some special presentation modes and the associated custom label. */
252 private static String getPresentationString(
253 Context context, int presentation, String customLabel) {
254 String name = context.getString(R.string.unknown);
255 if (!TextUtils.isEmpty(customLabel)
256 && ((presentation == TelecomManager.PRESENTATION_UNKNOWN)
257 || (presentation == TelecomManager.PRESENTATION_RESTRICTED))) {
258 name = customLabel;
259 return name;
260 } else {
261 if (presentation == TelecomManager.PRESENTATION_RESTRICTED) {
262 name = PhoneNumberHelper.getDisplayNameForRestrictedNumber(context).toString();
263 } else if (presentation == TelecomManager.PRESENTATION_PAYPHONE) {
264 name = context.getString(R.string.payphone);
265 }
266 }
267 return name;
268 }
269
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700270 ContactCacheEntry getInfo(String callId) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800271 return mInfoMap.get(callId);
272 }
273
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700274 void maybeInsertCnapInformationIntoCache(
Eric Erfanianccca3152017-02-22 16:32:36 -0800275 Context context, final DialerCall call, final CallerInfo info) {
276 final CachedNumberLookupService cachedNumberLookupService =
277 PhoneNumberCache.get(context).getCachedNumberLookupService();
278 if (!UserManagerCompat.isUserUnlocked(context)) {
279 Log.i(TAG, "User locked, not inserting cnap info into cache");
280 return;
281 }
282 if (cachedNumberLookupService == null
283 || TextUtils.isEmpty(info.cnapName)
284 || mInfoMap.get(call.getId()) != null) {
285 return;
286 }
287 final Context applicationContext = context.getApplicationContext();
288 Log.i(TAG, "Found contact with CNAP name - inserting into cache");
289 new AsyncTask<Void, Void, Void>() {
290 @Override
291 protected Void doInBackground(Void... params) {
292 ContactInfo contactInfo = new ContactInfo();
293 CachedContactInfo cacheInfo = cachedNumberLookupService.buildCachedContactInfo(contactInfo);
294 cacheInfo.setSource(CachedContactInfo.SOURCE_TYPE_CNAP, "CNAP", 0);
295 contactInfo.name = info.cnapName;
296 contactInfo.number = call.getNumber();
297 contactInfo.type = ContactsContract.CommonDataKinds.Phone.TYPE_MAIN;
298 try {
299 final JSONObject contactRows =
300 new JSONObject()
301 .put(
302 Phone.CONTENT_ITEM_TYPE,
303 new JSONObject()
304 .put(Phone.NUMBER, contactInfo.number)
305 .put(Phone.TYPE, Phone.TYPE_MAIN));
306 final String jsonString =
307 new JSONObject()
308 .put(Contacts.DISPLAY_NAME, contactInfo.name)
309 .put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME)
310 .put(Contacts.CONTENT_ITEM_TYPE, contactRows)
311 .toString();
312 cacheInfo.setLookupKey(jsonString);
313 } catch (JSONException e) {
314 Log.w(TAG, "Creation of lookup key failed when caching CNAP information");
315 }
316 cachedNumberLookupService.addContact(applicationContext, cacheInfo);
317 return null;
318 }
319 }.execute();
320 }
321
322 /**
323 * Requests contact data for the DialerCall object passed in. Returns the data through callback.
324 * If callback is null, no response is made, however the query is still performed and cached.
325 *
326 * @param callback The function to call back when the call is found. Can be null.
327 */
328 @MainThread
329 public void findInfo(
330 @NonNull final DialerCall call,
331 final boolean isIncoming,
332 @NonNull ContactInfoCacheCallback callback) {
333 Assert.isMainThread();
334 Objects.requireNonNull(callback);
335
336 final String callId = call.getId();
337 final ContactCacheEntry cacheEntry = mInfoMap.get(callId);
338 Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
339
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700340 // We need to force a new query if phone number has changed.
341 boolean forceQuery = needForceQuery(call, cacheEntry);
342 Log.d(TAG, "findInfo: callId = " + callId + "; forceQuery = " + forceQuery);
343
344 // If we have a previously obtained intermediate result return that now except needs
345 // force query.
346 if (cacheEntry != null && !forceQuery) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800347 Log.d(
348 TAG,
349 "Contact lookup. In memory cache hit; lookup "
350 + (callBacks == null ? "complete" : "still running"));
351 callback.onContactInfoComplete(callId, cacheEntry);
352 // If no other callbacks are in flight, we're done.
353 if (callBacks == null) {
354 return;
355 }
356 }
357
358 // If the entry already exists, add callback
359 if (callBacks != null) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700360 Log.d(TAG, "Another query is in progress, add callback only.");
Eric Erfanianccca3152017-02-22 16:32:36 -0800361 callBacks.add(callback);
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700362 if (!forceQuery) {
363 Log.d(TAG, "No need to query again, just return and wait for existing query to finish");
364 return;
365 }
366 } else {
367 Log.d(TAG, "Contact lookup. In memory cache miss; searching provider.");
368 // New lookup
369 callBacks = new ArraySet<>();
370 callBacks.add(callback);
371 mCallBacks.put(callId, callBacks);
Eric Erfanianccca3152017-02-22 16:32:36 -0800372 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800373
374 /**
375 * Performs a query for caller information. Save any immediate data we get from the query. An
376 * asynchronous query may also be made for any data that we do not already have. Some queries,
377 * such as those for voicemail and emergency call information, will not perform an additional
378 * asynchronous query.
379 */
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700380 final CallerInfoQueryToken queryToken = new CallerInfoQueryToken(mQueryId, callId);
381 mQueryId++;
Eric Erfanianccca3152017-02-22 16:32:36 -0800382 final CallerInfo callerInfo =
383 CallerInfoUtils.getCallerInfoForCall(
384 mContext,
385 call,
386 new DialerCallCookieWrapper(callId, call.getNumberPresentation()),
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700387 new FindInfoCallback(isIncoming, queryToken));
Eric Erfanianccca3152017-02-22 16:32:36 -0800388
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700389 if (cacheEntry != null) {
390 // We should not override the old cache item until the new query is
391 // back. We should only update the queryId. Otherwise, we may see
392 // flicker of the name and image (old cache -> new cache before query
393 // -> new cache after query)
394 cacheEntry.queryId = queryToken.mQueryId;
395 Log.d(TAG, "There is an existing cache. Do not override until new query is back");
396 } else {
397 ContactCacheEntry initialCacheEntry =
398 updateCallerInfoInCacheOnAnyThread(
399 callId, call.getNumberPresentation(), callerInfo, isIncoming, false, queryToken);
400 sendInfoNotifications(callId, initialCacheEntry);
401 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800402 }
403
404 @AnyThread
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700405 private ContactCacheEntry updateCallerInfoInCacheOnAnyThread(
Eric Erfanianccca3152017-02-22 16:32:36 -0800406 String callId,
407 int numberPresentation,
408 CallerInfo callerInfo,
409 boolean isIncoming,
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700410 boolean didLocalLookup,
411 CallerInfoQueryToken queryToken) {
412 Log.d(
413 TAG,
414 "updateCallerInfoInCacheOnAnyThread: callId = "
415 + callId
416 + "; queryId = "
417 + queryToken.mQueryId
418 + "; didLocalLookup = "
419 + didLocalLookup);
420
Eric Erfanianccca3152017-02-22 16:32:36 -0800421 int presentationMode = numberPresentation;
422 if (callerInfo.contactExists
423 || callerInfo.isEmergencyNumber()
424 || callerInfo.isVoiceMailNumber()) {
425 presentationMode = TelecomManager.PRESENTATION_ALLOWED;
426 }
427
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700428 // We always replace the entry. The only exception is the same photo case.
429 ContactCacheEntry cacheEntry = buildEntry(mContext, callerInfo, presentationMode, isIncoming);
430 cacheEntry.queryId = queryToken.mQueryId;
431
432 ContactCacheEntry existingCacheEntry = mInfoMap.get(callId);
433 Log.d(TAG, "Existing cacheEntry in hashMap " + existingCacheEntry);
434
435 if (didLocalLookup) {
436 // Before issuing a request for more data from other services, we only check that the
437 // contact wasn't found in the local DB. We don't check the if the cache entry already
438 // has a name because we allow overriding cnap data with data from other services.
439 if (!callerInfo.contactExists && mPhoneNumberService != null) {
440 Log.d(TAG, "Contact lookup. Local contacts miss, checking remote");
441 final PhoneNumberServiceListener listener =
442 new PhoneNumberServiceListener(callId, queryToken.mQueryId);
443 cacheEntry.hasPendingQuery = true;
444 mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener, isIncoming);
445 } else if (cacheEntry.displayPhotoUri != null) {
446 // When the difference between 2 numbers is only the prefix (e.g. + or IDD),
447 // we will still trigger force query so that the number can be updated on
448 // the calling screen. We need not query the image again if the previous
449 // query already has the image to avoid flickering.
450 if (existingCacheEntry != null
451 && existingCacheEntry.displayPhotoUri != null
452 && existingCacheEntry.displayPhotoUri.equals(cacheEntry.displayPhotoUri)
453 && existingCacheEntry.photo != null) {
454 Log.d(TAG, "Same picture. Do not need start image load.");
455 cacheEntry.photo = existingCacheEntry.photo;
456 cacheEntry.photoType = existingCacheEntry.photoType;
457 return cacheEntry;
Eric Erfanianccca3152017-02-22 16:32:36 -0800458 }
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700459
460 Log.d(TAG, "Contact lookup. Local contact found, starting image load");
461 // Load the image with a callback to update the image state.
462 // When the load is finished, onImageLoadComplete() will be called.
463 cacheEntry.hasPendingQuery = true;
464 ContactsAsyncHelper.startObtainPhotoAsync(
465 TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
466 mContext,
467 cacheEntry.displayPhotoUri,
468 ContactInfoCache.this,
469 queryToken);
Eric Erfanianccca3152017-02-22 16:32:36 -0800470 }
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700471 Log.d(TAG, "put entry into map: " + cacheEntry);
472 mInfoMap.put(callId, cacheEntry);
473 } else {
474 // Don't overwrite if there is existing cache.
475 Log.d(TAG, "put entry into map if not exists: " + cacheEntry);
476 mInfoMap.putIfAbsent(callId, cacheEntry);
Eric Erfanianccca3152017-02-22 16:32:36 -0800477 }
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700478 return cacheEntry;
Eric Erfanianccca3152017-02-22 16:32:36 -0800479 }
480
Eric Erfanian9779f962017-03-27 12:31:48 -0700481 private void maybeUpdateFromCequintCallerId(CallerInfo callerInfo, boolean isIncoming) {
482 if (!CequintCallerIdManager.isCequintCallerIdEnabled(mContext)) {
483 return;
484 }
485 CequintCallerIdContact cequintCallerIdContact =
486 CequintCallerIdManager.getCequintCallerIdContactForInCall(
487 mContext, callerInfo.phoneNumber, callerInfo.cnapName, isIncoming);
488
489 if (!TextUtils.isEmpty(cequintCallerIdContact.name)) {
490 callerInfo.name = cequintCallerIdContact.name;
491 }
492 if (!TextUtils.isEmpty(cequintCallerIdContact.geoDescription)) {
493 callerInfo.geoDescription = cequintCallerIdContact.geoDescription;
494 }
495 if (cequintCallerIdContact.imageUrl != null) {
496 callerInfo.contactDisplayPhotoUri = Uri.parse(cequintCallerIdContact.imageUrl);
497 }
498 }
499
Eric Erfanianccca3152017-02-22 16:32:36 -0800500 /**
501 * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface. Update contact photo
502 * when image is loaded in worker thread.
503 */
504 @WorkerThread
505 @Override
506 public void onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
507 Assert.isWorkerThread();
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700508 CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
509 final String callId = myCookie.mCallId;
510 final int queryId = myCookie.mQueryId;
511 if (!isWaitingForThisQuery(callId, queryId)) {
512 return;
513 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800514 loadImage(photo, photoIcon, cookie);
515 }
516
517 private void loadImage(Drawable photo, Bitmap photoIcon, Object cookie) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700518 Log.d(TAG, "Image load complete with context: ", mContext);
Eric Erfanianccca3152017-02-22 16:32:36 -0800519 // TODO: may be nice to update the image view again once the newer one
520 // is available on contacts database.
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700521 CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
522 final String callId = myCookie.mCallId;
Eric Erfanianccca3152017-02-22 16:32:36 -0800523 ContactCacheEntry entry = mInfoMap.get(callId);
524
525 if (entry == null) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700526 Log.e(TAG, "Image Load received for empty search entry.");
Eric Erfanianccca3152017-02-22 16:32:36 -0800527 clearCallbacks(callId);
528 return;
529 }
530
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700531 Log.d(TAG, "setting photo for entry: ", entry);
Eric Erfanianccca3152017-02-22 16:32:36 -0800532
533 // Conference call icons are being handled in CallCardPresenter.
534 if (photo != null) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700535 Log.v(TAG, "direct drawable: ", photo);
Eric Erfanianccca3152017-02-22 16:32:36 -0800536 entry.photo = photo;
537 entry.photoType = ContactPhotoType.CONTACT;
538 } else if (photoIcon != null) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700539 Log.v(TAG, "photo icon: ", photoIcon);
Eric Erfanianccca3152017-02-22 16:32:36 -0800540 entry.photo = new BitmapDrawable(mContext.getResources(), photoIcon);
541 entry.photoType = ContactPhotoType.CONTACT;
542 } else {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700543 Log.v(TAG, "unknown photo");
Eric Erfanianccca3152017-02-22 16:32:36 -0800544 entry.photo = null;
545 entry.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
546 }
547 }
548
549 /**
550 * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface. make sure that the
551 * call state is reflected after the image is loaded.
552 */
553 @MainThread
554 @Override
555 public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
556 Assert.isMainThread();
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700557 CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
558 final String callId = myCookie.mCallId;
559 final int queryId = myCookie.mQueryId;
560 if (!isWaitingForThisQuery(callId, queryId)) {
561 return;
562 }
563 sendImageNotifications(callId, mInfoMap.get(callId));
Eric Erfanianccca3152017-02-22 16:32:36 -0800564
565 clearCallbacks(callId);
566 }
567
568 /** Blows away the stored cache values. */
569 public void clearCache() {
570 mInfoMap.clear();
571 mCallBacks.clear();
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700572 mQueryId = 0;
Eric Erfanianccca3152017-02-22 16:32:36 -0800573 }
574
575 private ContactCacheEntry buildEntry(
576 Context context, CallerInfo info, int presentation, boolean isIncoming) {
577 final ContactCacheEntry cce = new ContactCacheEntry();
578 populateCacheEntry(context, info, cce, presentation, isIncoming);
579
580 // This will only be true for emergency numbers
581 if (info.photoResource != 0) {
582 cce.photo = context.getResources().getDrawable(info.photoResource);
583 } else if (info.isCachedPhotoCurrent) {
584 if (info.cachedPhoto != null) {
585 cce.photo = info.cachedPhoto;
586 cce.photoType = ContactPhotoType.CONTACT;
587 } else {
588 cce.photo = getDefaultContactPhotoDrawable();
589 cce.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
590 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800591 } else {
592 cce.displayPhotoUri = info.contactDisplayPhotoUri;
593 cce.photo = null;
594 }
595
596 // Support any contact id in N because QuickContacts in N starts supporting enterprise
597 // contact id
598 if (info.lookupKeyOrNull != null
599 && (VERSION.SDK_INT >= VERSION_CODES.N || info.contactIdOrZero != 0)) {
600 cce.lookupUri = Contacts.getLookupUri(info.contactIdOrZero, info.lookupKeyOrNull);
601 } else {
602 Log.v(TAG, "lookup key is null or contact ID is 0 on M. Don't create a lookup uri.");
603 cce.lookupUri = null;
604 }
605
606 cce.lookupKey = info.lookupKeyOrNull;
607 cce.contactRingtoneUri = info.contactRingtoneUri;
608 if (cce.contactRingtoneUri == null || Uri.EMPTY.equals(cce.contactRingtoneUri)) {
609 cce.contactRingtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
610 }
611
612 return cce;
613 }
614
615 /** Sends the updated information to call the callbacks for the entry. */
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700616 @MainThread
Eric Erfanianccca3152017-02-22 16:32:36 -0800617 private void sendInfoNotifications(String callId, ContactCacheEntry entry) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700618 Assert.isMainThread();
Eric Erfanianccca3152017-02-22 16:32:36 -0800619 final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
620 if (callBacks != null) {
621 for (ContactInfoCacheCallback callBack : callBacks) {
622 callBack.onContactInfoComplete(callId, entry);
623 }
624 }
625 }
626
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700627 @MainThread
Eric Erfanianccca3152017-02-22 16:32:36 -0800628 private void sendImageNotifications(String callId, ContactCacheEntry entry) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700629 Assert.isMainThread();
Eric Erfanianccca3152017-02-22 16:32:36 -0800630 final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
631 if (callBacks != null && entry.photo != null) {
632 for (ContactInfoCacheCallback callBack : callBacks) {
633 callBack.onImageLoadComplete(callId, entry);
634 }
635 }
636 }
637
638 private void clearCallbacks(String callId) {
639 mCallBacks.remove(callId);
640 }
641
642 public Drawable getDefaultContactPhotoDrawable() {
643 if (mDefaultContactPhotoDrawable == null) {
644 mDefaultContactPhotoDrawable =
645 mContext.getResources().getDrawable(R.drawable.img_no_image_automirrored);
646 }
647 return mDefaultContactPhotoDrawable;
648 }
649
650 public Drawable getConferenceDrawable() {
651 if (mConferencePhotoDrawable == null) {
652 mConferencePhotoDrawable =
653 mContext.getResources().getDrawable(R.drawable.img_conference_automirrored);
654 }
655 return mConferencePhotoDrawable;
656 }
657
658 /** Callback interface for the contact query. */
659 public interface ContactInfoCacheCallback {
660
661 void onContactInfoComplete(String callId, ContactCacheEntry entry);
662
663 void onImageLoadComplete(String callId, ContactCacheEntry entry);
664 }
665
666 /** This is cached contact info, which should be the ONLY info used by UI. */
667 public static class ContactCacheEntry {
668
669 public String namePrimary;
670 public String nameAlternative;
671 public String number;
672 public String location;
673 public String label;
674 public Drawable photo;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700675 @ContactPhotoType int photoType;
676 boolean isSipCall;
Eric Erfanianccca3152017-02-22 16:32:36 -0800677 // Note in cache entry whether this is a pending async loading action to know whether to
678 // wait for its callback or not.
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700679 boolean hasPendingQuery;
Eric Erfanianccca3152017-02-22 16:32:36 -0800680 /** This will be used for the "view" notification. */
681 public Uri contactUri;
682 /** Either a display photo or a thumbnail URI. */
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700683 Uri displayPhotoUri;
Eric Erfanianccca3152017-02-22 16:32:36 -0800684
685 public Uri lookupUri; // Sent to NotificationMananger
686 public String lookupKey;
687 public int contactLookupResult = ContactLookupResult.Type.NOT_FOUND;
688 public long userType = ContactsUtils.USER_TYPE_CURRENT;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700689 Uri contactRingtoneUri;
690 /** Query id to identify the query session. */
691 int queryId;
692 /** The phone number without any changes to display to the user (ex: cnap...) */
693 String originalPhoneNumber;
Eric Erfanian9779f962017-03-27 12:31:48 -0700694
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700695 boolean isBusiness;
Eric Erfanianccca3152017-02-22 16:32:36 -0800696
697 @Override
698 public String toString() {
699 return "ContactCacheEntry{"
700 + "name='"
701 + MoreStrings.toSafeString(namePrimary)
702 + '\''
703 + ", nameAlternative='"
704 + MoreStrings.toSafeString(nameAlternative)
705 + '\''
706 + ", number='"
707 + MoreStrings.toSafeString(number)
708 + '\''
709 + ", location='"
710 + MoreStrings.toSafeString(location)
711 + '\''
712 + ", label='"
713 + label
714 + '\''
715 + ", photo="
716 + photo
717 + ", isSipCall="
718 + isSipCall
719 + ", contactUri="
720 + contactUri
721 + ", displayPhotoUri="
722 + displayPhotoUri
723 + ", contactLookupResult="
724 + contactLookupResult
725 + ", userType="
726 + userType
727 + ", contactRingtoneUri="
728 + contactRingtoneUri
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700729 + ", queryId="
730 + queryId
731 + ", originalPhoneNumber="
732 + originalPhoneNumber
Eric Erfanianccca3152017-02-22 16:32:36 -0800733 + '}';
734 }
735 }
736
737 private static final class DialerCallCookieWrapper {
738 public final String callId;
739 public final int numberPresentation;
740
741 public DialerCallCookieWrapper(String callId, int numberPresentation) {
742 this.callId = callId;
743 this.numberPresentation = numberPresentation;
744 }
745 }
746
747 private class FindInfoCallback implements OnQueryCompleteListener {
748
749 private final boolean mIsIncoming;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700750 private final CallerInfoQueryToken mQueryToken;
Eric Erfanianccca3152017-02-22 16:32:36 -0800751
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700752 public FindInfoCallback(boolean isIncoming, CallerInfoQueryToken queryToken) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800753 mIsIncoming = isIncoming;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700754 mQueryToken = queryToken;
Eric Erfanianccca3152017-02-22 16:32:36 -0800755 }
756
757 @Override
758 public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
759 Assert.isWorkerThread();
760 DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700761 if (!isWaitingForThisQuery(cw.callId, mQueryToken.mQueryId)) {
762 return;
763 }
Eric Erfanian9779f962017-03-27 12:31:48 -0700764 long start = SystemClock.uptimeMillis();
765 maybeUpdateFromCequintCallerId(ci, mIsIncoming);
766 long time = SystemClock.uptimeMillis() - start;
767 Log.d(TAG, "Cequint Caller Id look up takes " + time + " ms.");
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700768 updateCallerInfoInCacheOnAnyThread(
769 cw.callId, cw.numberPresentation, ci, mIsIncoming, true, mQueryToken);
Eric Erfanianccca3152017-02-22 16:32:36 -0800770 }
771
772 @Override
773 public void onQueryComplete(int token, Object cookie, CallerInfo callerInfo) {
774 Assert.isMainThread();
775 DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie;
776 String callId = cw.callId;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700777 if (!isWaitingForThisQuery(cw.callId, mQueryToken.mQueryId)) {
778 return;
779 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800780 ContactCacheEntry cacheEntry = mInfoMap.get(callId);
781 // This may happen only when InCallPresenter attempt to cleanup.
782 if (cacheEntry == null) {
783 Log.w(TAG, "Contact lookup done, but cache entry is not found.");
784 clearCallbacks(callId);
785 return;
786 }
787 sendInfoNotifications(callId, cacheEntry);
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700788 if (!cacheEntry.hasPendingQuery) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800789 if (callerInfo.contactExists) {
790 Log.d(TAG, "Contact lookup done. Local contact found, no image.");
791 } else {
792 Log.d(
793 TAG,
794 "Contact lookup done. Local contact not found and"
795 + " no remote lookup service available.");
796 }
797 clearCallbacks(callId);
798 }
799 }
800 }
801
802 class PhoneNumberServiceListener
803 implements PhoneNumberService.NumberLookupListener, PhoneNumberService.ImageLookupListener {
804
805 private final String mCallId;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700806 private final int mQueryIdOfRemoteLookup;
Eric Erfanianccca3152017-02-22 16:32:36 -0800807
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700808 PhoneNumberServiceListener(String callId, int queryId) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800809 mCallId = callId;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700810 mQueryIdOfRemoteLookup = queryId;
Eric Erfanianccca3152017-02-22 16:32:36 -0800811 }
812
813 @Override
814 public void onPhoneNumberInfoComplete(final PhoneNumberService.PhoneNumberInfo info) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700815 Log.d(TAG, "PhoneNumberServiceListener.onPhoneNumberInfoComplete");
816 if (!isWaitingForThisQuery(mCallId, mQueryIdOfRemoteLookup)) {
817 return;
818 }
819
Eric Erfanianccca3152017-02-22 16:32:36 -0800820 // If we got a miss, this is the end of the lookup pipeline,
821 // so clear the callbacks and return.
822 if (info == null) {
823 Log.d(TAG, "Contact lookup done. Remote contact not found.");
824 clearCallbacks(mCallId);
825 return;
826 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800827 ContactCacheEntry entry = new ContactCacheEntry();
828 entry.namePrimary = info.getDisplayName();
829 entry.number = info.getNumber();
830 entry.contactLookupResult = info.getLookupSource();
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700831 entry.isBusiness = info.isBusiness();
Eric Erfanianccca3152017-02-22 16:32:36 -0800832 final int type = info.getPhoneType();
833 final String label = info.getPhoneLabel();
834 if (type == Phone.TYPE_CUSTOM) {
835 entry.label = label;
836 } else {
837 final CharSequence typeStr = Phone.getTypeLabel(mContext.getResources(), type, label);
838 entry.label = typeStr == null ? null : typeStr.toString();
839 }
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700840 final ContactCacheEntry oldEntry = mInfoMap.get(mCallId);
841 if (oldEntry != null) {
842 // Location is only obtained from local lookup so persist
843 // the value for remote lookups. Once we have a name this
844 // field is no longer used; it is persisted here in case
845 // the UI is ever changed to use it.
846 entry.location = oldEntry.location;
847 // Contact specific ringtone is obtained from local lookup.
848 entry.contactRingtoneUri = oldEntry.contactRingtoneUri;
Eric Erfanianccca3152017-02-22 16:32:36 -0800849 }
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700850
851 // If no image and it's a business, switch to using the default business avatar.
852 if (info.getImageUrl() == null && info.isBusiness()) {
853 Log.d(TAG, "Business has no image. Using default.");
854 entry.photo = mContext.getResources().getDrawable(R.drawable.img_business);
855 entry.photoType = ContactPhotoType.BUSINESS;
856 }
857
858 Log.d(TAG, "put entry into map: " + entry);
859 mInfoMap.put(mCallId, entry);
Eric Erfanianccca3152017-02-22 16:32:36 -0800860 sendInfoNotifications(mCallId, entry);
861
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700862 entry.hasPendingQuery = info.getImageUrl() != null;
Eric Erfanianccca3152017-02-22 16:32:36 -0800863
864 // If there is no image then we should not expect another callback.
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700865 if (!entry.hasPendingQuery) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800866 // We're done, so clear callbacks
867 clearCallbacks(mCallId);
868 }
869 }
870
871 @Override
872 public void onImageFetchComplete(Bitmap bitmap) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700873 Log.d(TAG, "PhoneNumberServiceListener.onImageFetchComplete");
874 if (!isWaitingForThisQuery(mCallId, mQueryIdOfRemoteLookup)) {
875 return;
876 }
877 CallerInfoQueryToken queryToken = new CallerInfoQueryToken(mQueryIdOfRemoteLookup, mCallId);
878 loadImage(null, bitmap, queryToken);
879 onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, queryToken);
880 }
881 }
882
883 private boolean needForceQuery(DialerCall call, ContactCacheEntry cacheEntry) {
884 if (call == null || call.isConferenceCall()) {
885 return false;
886 }
887
888 String newPhoneNumber = PhoneNumberUtils.stripSeparators(call.getNumber());
889 if (cacheEntry == null) {
890 // No info in the map yet so it is the 1st query
891 Log.d(TAG, "needForceQuery: first query");
892 return true;
893 }
894 String oldPhoneNumber = PhoneNumberUtils.stripSeparators(cacheEntry.originalPhoneNumber);
895
896 if (!TextUtils.equals(oldPhoneNumber, newPhoneNumber)) {
897 Log.d(TAG, "phone number has changed: " + oldPhoneNumber + " -> " + newPhoneNumber);
898 return true;
899 }
900
901 return false;
902 }
903
904 private static final class CallerInfoQueryToken {
905 final int mQueryId;
906 final String mCallId;
907
908 CallerInfoQueryToken(int queryId, String callId) {
909 mQueryId = queryId;
910 mCallId = callId;
911 }
912 }
913
914 /** Check if the queryId in the cached map is the same as the one from query result. */
915 private boolean isWaitingForThisQuery(String callId, int queryId) {
916 final ContactCacheEntry existingCacheEntry = mInfoMap.get(callId);
917 if (existingCacheEntry == null) {
918 // This might happen if lookup on background thread comes back before the initial entry is
919 // created.
920 Log.d(TAG, "Cached entry is null.");
921 return true;
922 } else {
923 int waitingQueryId = existingCacheEntry.queryId;
924 Log.d(TAG, "waitingQueryId = " + waitingQueryId + "; queryId = " + queryId);
925 return waitingQueryId == queryId;
Eric Erfanianccca3152017-02-22 16:32:36 -0800926 }
927 }
928}