blob: c4e25e7009a6c3b75cb5b8f6196f891084335fa5 [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;
28import android.provider.ContactsContract;
29import android.provider.ContactsContract.CommonDataKinds.Phone;
30import android.provider.ContactsContract.Contacts;
31import android.provider.ContactsContract.DisplayNameSources;
32import android.support.annotation.AnyThread;
33import android.support.annotation.MainThread;
34import android.support.annotation.NonNull;
35import android.support.annotation.WorkerThread;
36import android.support.v4.os.UserManagerCompat;
37import android.telecom.TelecomManager;
Eric Erfaniand5e47f62017-03-15 14:41:07 -070038import android.telephony.PhoneNumberUtils;
Eric Erfanianccca3152017-02-22 16:32:36 -080039import android.text.TextUtils;
40import android.util.ArrayMap;
41import android.util.ArraySet;
42import com.android.contacts.common.ContactsUtils;
43import com.android.dialer.common.Assert;
44import com.android.dialer.logging.nano.ContactLookupResult;
45import com.android.dialer.phonenumbercache.CachedNumberLookupService;
46import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
47import com.android.dialer.phonenumbercache.ContactInfo;
48import com.android.dialer.phonenumbercache.PhoneNumberCache;
49import com.android.dialer.phonenumberutil.PhoneNumberHelper;
50import com.android.dialer.util.MoreStrings;
51import com.android.incallui.CallerInfoAsyncQuery.OnQueryCompleteListener;
52import com.android.incallui.ContactsAsyncHelper.OnImageLoadCompleteListener;
53import com.android.incallui.bindings.PhoneNumberService;
54import com.android.incallui.call.DialerCall;
55import com.android.incallui.incall.protocol.ContactPhotoType;
56import java.util.Map;
57import java.util.Objects;
58import java.util.Set;
59import java.util.concurrent.ConcurrentHashMap;
60import org.json.JSONException;
61import org.json.JSONObject;
62
63/**
64 * Class responsible for querying Contact Information for DialerCall objects. Can perform
65 * asynchronous requests to the Contact Provider for information as well as respond synchronously
66 * for any data that it currently has cached from previous queries. This class always gets called
67 * from the UI thread so it does not need thread protection.
68 */
69public class ContactInfoCache implements OnImageLoadCompleteListener {
70
71 private static final String TAG = ContactInfoCache.class.getSimpleName();
72 private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0;
73 private static ContactInfoCache sCache = null;
74 private final Context mContext;
75 private final PhoneNumberService mPhoneNumberService;
76 // Cache info map needs to be thread-safe since it could be modified by both main thread and
77 // worker thread.
Eric Erfaniand5e47f62017-03-15 14:41:07 -070078 private final ConcurrentHashMap<String, ContactCacheEntry> mInfoMap = new ConcurrentHashMap<>();
Eric Erfanianccca3152017-02-22 16:32:36 -080079 private final Map<String, Set<ContactInfoCacheCallback>> mCallBacks = new ArrayMap<>();
80 private Drawable mDefaultContactPhotoDrawable;
81 private Drawable mConferencePhotoDrawable;
Eric Erfaniand5e47f62017-03-15 14:41:07 -070082 private int mQueryId;
Eric Erfanianccca3152017-02-22 16:32:36 -080083
84 private ContactInfoCache(Context context) {
85 mContext = context;
86 mPhoneNumberService = Bindings.get(context).newPhoneNumberService(context);
87 }
88
89 public static synchronized ContactInfoCache getInstance(Context mContext) {
90 if (sCache == null) {
91 sCache = new ContactInfoCache(mContext.getApplicationContext());
92 }
93 return sCache;
94 }
95
Eric Erfaniand5e47f62017-03-15 14:41:07 -070096 static ContactCacheEntry buildCacheEntryFromCall(
Eric Erfanianccca3152017-02-22 16:32:36 -080097 Context context, DialerCall call, boolean isIncoming) {
98 final ContactCacheEntry entry = new ContactCacheEntry();
99
100 // TODO: get rid of caller info.
101 final CallerInfo info = CallerInfoUtils.buildCallerInfo(context, call);
102 ContactInfoCache.populateCacheEntry(
103 context, info, entry, call.getNumberPresentation(), isIncoming);
104 return entry;
105 }
106
107 /** Populate a cache entry from a call (which got converted into a caller info). */
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700108 private static void populateCacheEntry(
Eric Erfanianccca3152017-02-22 16:32:36 -0800109 @NonNull Context context,
110 @NonNull CallerInfo info,
111 @NonNull ContactCacheEntry cce,
112 int presentation,
113 boolean isIncoming) {
114 Objects.requireNonNull(info);
115 String displayName = null;
116 String displayNumber = null;
117 String displayLocation = null;
118 String label = null;
119 boolean isSipCall = false;
120
121 // It appears that there is a small change in behaviour with the
122 // PhoneUtils' startGetCallerInfo whereby if we query with an
123 // empty number, we will get a valid CallerInfo object, but with
124 // fields that are all null, and the isTemporary boolean input
125 // parameter as true.
126
127 // In the past, we would see a NULL callerinfo object, but this
128 // ends up causing null pointer exceptions elsewhere down the
129 // line in other cases, so we need to make this fix instead. It
130 // appears that this was the ONLY call to PhoneUtils
131 // .getCallerInfo() that relied on a NULL CallerInfo to indicate
132 // an unknown contact.
133
134 // Currently, info.phoneNumber may actually be a SIP address, and
135 // if so, it might sometimes include the "sip:" prefix. That
136 // prefix isn't really useful to the user, though, so strip it off
137 // if present. (For any other URI scheme, though, leave the
138 // prefix alone.)
139 // TODO: It would be cleaner for CallerInfo to explicitly support
140 // SIP addresses instead of overloading the "phoneNumber" field.
141 // Then we could remove this hack, and instead ask the CallerInfo
142 // for a "user visible" form of the SIP address.
143 String number = info.phoneNumber;
144
145 if (!TextUtils.isEmpty(number)) {
146 isSipCall = PhoneNumberHelper.isUriNumber(number);
147 if (number.startsWith("sip:")) {
148 number = number.substring(4);
149 }
150 }
151
152 if (TextUtils.isEmpty(info.name)) {
153 // No valid "name" in the CallerInfo, so fall back to
154 // something else.
155 // (Typically, we promote the phone number up to the "name" slot
156 // onscreen, and possibly display a descriptive string in the
157 // "number" slot.)
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700158 if (TextUtils.isEmpty(number) && TextUtils.isEmpty(info.cnapName)) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800159 // No name *or* number! Display a generic "unknown" string
160 // (or potentially some other default based on the presentation.)
161 displayName = getPresentationString(context, presentation, info.callSubject);
162 Log.d(TAG, " ==> no name *or* number! displayName = " + displayName);
163 } else if (presentation != TelecomManager.PRESENTATION_ALLOWED) {
164 // This case should never happen since the network should never send a phone #
165 // AND a restricted presentation. However we leave it here in case of weird
166 // network behavior
167 displayName = getPresentationString(context, presentation, info.callSubject);
168 Log.d(TAG, " ==> presentation not allowed! displayName = " + displayName);
169 } else if (!TextUtils.isEmpty(info.cnapName)) {
170 // No name, but we do have a valid CNAP name, so use that.
171 displayName = info.cnapName;
172 info.name = info.cnapName;
173 displayNumber = PhoneNumberHelper.formatNumber(number, context);
174 Log.d(
175 TAG,
176 " ==> cnapName available: displayName '"
177 + displayName
178 + "', displayNumber '"
179 + displayNumber
180 + "'");
181 } else {
182 // No name; all we have is a number. This is the typical
183 // case when an incoming call doesn't match any contact,
184 // or if you manually dial an outgoing number using the
185 // dialpad.
186 displayNumber = PhoneNumberHelper.formatNumber(number, context);
187
188 // Display a geographical description string if available
189 // (but only for incoming calls.)
190 if (isIncoming) {
191 // TODO (CallerInfoAsyncQuery cleanup): Fix the CallerInfo
192 // query to only do the geoDescription lookup in the first
193 // place for incoming calls.
194 displayLocation = info.geoDescription; // may be null
195 Log.d(TAG, "Geodescrption: " + info.geoDescription);
196 }
197
198 Log.d(
199 TAG,
200 " ==> no name; falling back to number:"
201 + " displayNumber '"
202 + Log.pii(displayNumber)
203 + "', displayLocation '"
204 + displayLocation
205 + "'");
206 }
207 } else {
208 // We do have a valid "name" in the CallerInfo. Display that
209 // in the "name" slot, and the phone number in the "number" slot.
210 if (presentation != TelecomManager.PRESENTATION_ALLOWED) {
211 // This case should never happen since the network should never send a name
212 // AND a restricted presentation. However we leave it here in case of weird
213 // network behavior
214 displayName = getPresentationString(context, presentation, info.callSubject);
215 Log.d(
216 TAG,
217 " ==> valid name, but presentation not allowed!" + " displayName = " + displayName);
218 } else {
219 // Causes cce.namePrimary to be set as info.name below. CallCardPresenter will
220 // later determine whether to use the name or nameAlternative when presenting
221 displayName = info.name;
222 cce.nameAlternative = info.nameAlternative;
223 displayNumber = PhoneNumberHelper.formatNumber(number, context);
224 label = info.phoneLabel;
225 Log.d(
226 TAG,
227 " ==> name is present in CallerInfo: displayName '"
228 + displayName
229 + "', displayNumber '"
230 + displayNumber
231 + "'");
232 }
233 }
234
235 cce.namePrimary = displayName;
236 cce.number = displayNumber;
237 cce.location = displayLocation;
238 cce.label = label;
239 cce.isSipCall = isSipCall;
240 cce.userType = info.userType;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700241 cce.originalPhoneNumber = info.phoneNumber;
Eric Erfanianccca3152017-02-22 16:32:36 -0800242
243 if (info.contactExists) {
244 cce.contactLookupResult = ContactLookupResult.Type.LOCAL_CONTACT;
245 }
246 }
247
248 /** Gets name strings based on some special presentation modes and the associated custom label. */
249 private static String getPresentationString(
250 Context context, int presentation, String customLabel) {
251 String name = context.getString(R.string.unknown);
252 if (!TextUtils.isEmpty(customLabel)
253 && ((presentation == TelecomManager.PRESENTATION_UNKNOWN)
254 || (presentation == TelecomManager.PRESENTATION_RESTRICTED))) {
255 name = customLabel;
256 return name;
257 } else {
258 if (presentation == TelecomManager.PRESENTATION_RESTRICTED) {
259 name = PhoneNumberHelper.getDisplayNameForRestrictedNumber(context).toString();
260 } else if (presentation == TelecomManager.PRESENTATION_PAYPHONE) {
261 name = context.getString(R.string.payphone);
262 }
263 }
264 return name;
265 }
266
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700267 ContactCacheEntry getInfo(String callId) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800268 return mInfoMap.get(callId);
269 }
270
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700271 void maybeInsertCnapInformationIntoCache(
Eric Erfanianccca3152017-02-22 16:32:36 -0800272 Context context, final DialerCall call, final CallerInfo info) {
273 final CachedNumberLookupService cachedNumberLookupService =
274 PhoneNumberCache.get(context).getCachedNumberLookupService();
275 if (!UserManagerCompat.isUserUnlocked(context)) {
276 Log.i(TAG, "User locked, not inserting cnap info into cache");
277 return;
278 }
279 if (cachedNumberLookupService == null
280 || TextUtils.isEmpty(info.cnapName)
281 || mInfoMap.get(call.getId()) != null) {
282 return;
283 }
284 final Context applicationContext = context.getApplicationContext();
285 Log.i(TAG, "Found contact with CNAP name - inserting into cache");
286 new AsyncTask<Void, Void, Void>() {
287 @Override
288 protected Void doInBackground(Void... params) {
289 ContactInfo contactInfo = new ContactInfo();
290 CachedContactInfo cacheInfo = cachedNumberLookupService.buildCachedContactInfo(contactInfo);
291 cacheInfo.setSource(CachedContactInfo.SOURCE_TYPE_CNAP, "CNAP", 0);
292 contactInfo.name = info.cnapName;
293 contactInfo.number = call.getNumber();
294 contactInfo.type = ContactsContract.CommonDataKinds.Phone.TYPE_MAIN;
295 try {
296 final JSONObject contactRows =
297 new JSONObject()
298 .put(
299 Phone.CONTENT_ITEM_TYPE,
300 new JSONObject()
301 .put(Phone.NUMBER, contactInfo.number)
302 .put(Phone.TYPE, Phone.TYPE_MAIN));
303 final String jsonString =
304 new JSONObject()
305 .put(Contacts.DISPLAY_NAME, contactInfo.name)
306 .put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME)
307 .put(Contacts.CONTENT_ITEM_TYPE, contactRows)
308 .toString();
309 cacheInfo.setLookupKey(jsonString);
310 } catch (JSONException e) {
311 Log.w(TAG, "Creation of lookup key failed when caching CNAP information");
312 }
313 cachedNumberLookupService.addContact(applicationContext, cacheInfo);
314 return null;
315 }
316 }.execute();
317 }
318
319 /**
320 * Requests contact data for the DialerCall object passed in. Returns the data through callback.
321 * If callback is null, no response is made, however the query is still performed and cached.
322 *
323 * @param callback The function to call back when the call is found. Can be null.
324 */
325 @MainThread
326 public void findInfo(
327 @NonNull final DialerCall call,
328 final boolean isIncoming,
329 @NonNull ContactInfoCacheCallback callback) {
330 Assert.isMainThread();
331 Objects.requireNonNull(callback);
332
333 final String callId = call.getId();
334 final ContactCacheEntry cacheEntry = mInfoMap.get(callId);
335 Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
336
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700337 // We need to force a new query if phone number has changed.
338 boolean forceQuery = needForceQuery(call, cacheEntry);
339 Log.d(TAG, "findInfo: callId = " + callId + "; forceQuery = " + forceQuery);
340
341 // If we have a previously obtained intermediate result return that now except needs
342 // force query.
343 if (cacheEntry != null && !forceQuery) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800344 Log.d(
345 TAG,
346 "Contact lookup. In memory cache hit; lookup "
347 + (callBacks == null ? "complete" : "still running"));
348 callback.onContactInfoComplete(callId, cacheEntry);
349 // If no other callbacks are in flight, we're done.
350 if (callBacks == null) {
351 return;
352 }
353 }
354
355 // If the entry already exists, add callback
356 if (callBacks != null) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700357 Log.d(TAG, "Another query is in progress, add callback only.");
Eric Erfanianccca3152017-02-22 16:32:36 -0800358 callBacks.add(callback);
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700359 if (!forceQuery) {
360 Log.d(TAG, "No need to query again, just return and wait for existing query to finish");
361 return;
362 }
363 } else {
364 Log.d(TAG, "Contact lookup. In memory cache miss; searching provider.");
365 // New lookup
366 callBacks = new ArraySet<>();
367 callBacks.add(callback);
368 mCallBacks.put(callId, callBacks);
Eric Erfanianccca3152017-02-22 16:32:36 -0800369 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800370
371 /**
372 * Performs a query for caller information. Save any immediate data we get from the query. An
373 * asynchronous query may also be made for any data that we do not already have. Some queries,
374 * such as those for voicemail and emergency call information, will not perform an additional
375 * asynchronous query.
376 */
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700377 final CallerInfoQueryToken queryToken = new CallerInfoQueryToken(mQueryId, callId);
378 mQueryId++;
Eric Erfanianccca3152017-02-22 16:32:36 -0800379 final CallerInfo callerInfo =
380 CallerInfoUtils.getCallerInfoForCall(
381 mContext,
382 call,
383 new DialerCallCookieWrapper(callId, call.getNumberPresentation()),
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700384 new FindInfoCallback(isIncoming, queryToken));
Eric Erfanianccca3152017-02-22 16:32:36 -0800385
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700386 if (cacheEntry != null) {
387 // We should not override the old cache item until the new query is
388 // back. We should only update the queryId. Otherwise, we may see
389 // flicker of the name and image (old cache -> new cache before query
390 // -> new cache after query)
391 cacheEntry.queryId = queryToken.mQueryId;
392 Log.d(TAG, "There is an existing cache. Do not override until new query is back");
393 } else {
394 ContactCacheEntry initialCacheEntry =
395 updateCallerInfoInCacheOnAnyThread(
396 callId, call.getNumberPresentation(), callerInfo, isIncoming, false, queryToken);
397 sendInfoNotifications(callId, initialCacheEntry);
398 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800399 }
400
401 @AnyThread
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700402 private ContactCacheEntry updateCallerInfoInCacheOnAnyThread(
Eric Erfanianccca3152017-02-22 16:32:36 -0800403 String callId,
404 int numberPresentation,
405 CallerInfo callerInfo,
406 boolean isIncoming,
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700407 boolean didLocalLookup,
408 CallerInfoQueryToken queryToken) {
409 Log.d(
410 TAG,
411 "updateCallerInfoInCacheOnAnyThread: callId = "
412 + callId
413 + "; queryId = "
414 + queryToken.mQueryId
415 + "; didLocalLookup = "
416 + didLocalLookup);
417
Eric Erfanianccca3152017-02-22 16:32:36 -0800418 int presentationMode = numberPresentation;
419 if (callerInfo.contactExists
420 || callerInfo.isEmergencyNumber()
421 || callerInfo.isVoiceMailNumber()) {
422 presentationMode = TelecomManager.PRESENTATION_ALLOWED;
423 }
424
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700425 // We always replace the entry. The only exception is the same photo case.
426 ContactCacheEntry cacheEntry = buildEntry(mContext, callerInfo, presentationMode, isIncoming);
427 cacheEntry.queryId = queryToken.mQueryId;
428
429 ContactCacheEntry existingCacheEntry = mInfoMap.get(callId);
430 Log.d(TAG, "Existing cacheEntry in hashMap " + existingCacheEntry);
431
432 if (didLocalLookup) {
433 // Before issuing a request for more data from other services, we only check that the
434 // contact wasn't found in the local DB. We don't check the if the cache entry already
435 // has a name because we allow overriding cnap data with data from other services.
436 if (!callerInfo.contactExists && mPhoneNumberService != null) {
437 Log.d(TAG, "Contact lookup. Local contacts miss, checking remote");
438 final PhoneNumberServiceListener listener =
439 new PhoneNumberServiceListener(callId, queryToken.mQueryId);
440 cacheEntry.hasPendingQuery = true;
441 mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener, isIncoming);
442 } else if (cacheEntry.displayPhotoUri != null) {
443 // When the difference between 2 numbers is only the prefix (e.g. + or IDD),
444 // we will still trigger force query so that the number can be updated on
445 // the calling screen. We need not query the image again if the previous
446 // query already has the image to avoid flickering.
447 if (existingCacheEntry != null
448 && existingCacheEntry.displayPhotoUri != null
449 && existingCacheEntry.displayPhotoUri.equals(cacheEntry.displayPhotoUri)
450 && existingCacheEntry.photo != null) {
451 Log.d(TAG, "Same picture. Do not need start image load.");
452 cacheEntry.photo = existingCacheEntry.photo;
453 cacheEntry.photoType = existingCacheEntry.photoType;
454 return cacheEntry;
Eric Erfanianccca3152017-02-22 16:32:36 -0800455 }
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700456
457 Log.d(TAG, "Contact lookup. Local contact found, starting image load");
458 // Load the image with a callback to update the image state.
459 // When the load is finished, onImageLoadComplete() will be called.
460 cacheEntry.hasPendingQuery = true;
461 ContactsAsyncHelper.startObtainPhotoAsync(
462 TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
463 mContext,
464 cacheEntry.displayPhotoUri,
465 ContactInfoCache.this,
466 queryToken);
Eric Erfanianccca3152017-02-22 16:32:36 -0800467 }
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700468 Log.d(TAG, "put entry into map: " + cacheEntry);
469 mInfoMap.put(callId, cacheEntry);
470 } else {
471 // Don't overwrite if there is existing cache.
472 Log.d(TAG, "put entry into map if not exists: " + cacheEntry);
473 mInfoMap.putIfAbsent(callId, cacheEntry);
Eric Erfanianccca3152017-02-22 16:32:36 -0800474 }
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700475 return cacheEntry;
Eric Erfanianccca3152017-02-22 16:32:36 -0800476 }
477
478 /**
479 * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface. Update contact photo
480 * when image is loaded in worker thread.
481 */
482 @WorkerThread
483 @Override
484 public void onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
485 Assert.isWorkerThread();
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700486 CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
487 final String callId = myCookie.mCallId;
488 final int queryId = myCookie.mQueryId;
489 if (!isWaitingForThisQuery(callId, queryId)) {
490 return;
491 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800492 loadImage(photo, photoIcon, cookie);
493 }
494
495 private void loadImage(Drawable photo, Bitmap photoIcon, Object cookie) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700496 Log.d(TAG, "Image load complete with context: ", mContext);
Eric Erfanianccca3152017-02-22 16:32:36 -0800497 // TODO: may be nice to update the image view again once the newer one
498 // is available on contacts database.
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700499 CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
500 final String callId = myCookie.mCallId;
Eric Erfanianccca3152017-02-22 16:32:36 -0800501 ContactCacheEntry entry = mInfoMap.get(callId);
502
503 if (entry == null) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700504 Log.e(TAG, "Image Load received for empty search entry.");
Eric Erfanianccca3152017-02-22 16:32:36 -0800505 clearCallbacks(callId);
506 return;
507 }
508
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700509 Log.d(TAG, "setting photo for entry: ", entry);
Eric Erfanianccca3152017-02-22 16:32:36 -0800510
511 // Conference call icons are being handled in CallCardPresenter.
512 if (photo != null) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700513 Log.v(TAG, "direct drawable: ", photo);
Eric Erfanianccca3152017-02-22 16:32:36 -0800514 entry.photo = photo;
515 entry.photoType = ContactPhotoType.CONTACT;
516 } else if (photoIcon != null) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700517 Log.v(TAG, "photo icon: ", photoIcon);
Eric Erfanianccca3152017-02-22 16:32:36 -0800518 entry.photo = new BitmapDrawable(mContext.getResources(), photoIcon);
519 entry.photoType = ContactPhotoType.CONTACT;
520 } else {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700521 Log.v(TAG, "unknown photo");
Eric Erfanianccca3152017-02-22 16:32:36 -0800522 entry.photo = null;
523 entry.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
524 }
525 }
526
527 /**
528 * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface. make sure that the
529 * call state is reflected after the image is loaded.
530 */
531 @MainThread
532 @Override
533 public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
534 Assert.isMainThread();
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700535 CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
536 final String callId = myCookie.mCallId;
537 final int queryId = myCookie.mQueryId;
538 if (!isWaitingForThisQuery(callId, queryId)) {
539 return;
540 }
541 sendImageNotifications(callId, mInfoMap.get(callId));
Eric Erfanianccca3152017-02-22 16:32:36 -0800542
543 clearCallbacks(callId);
544 }
545
546 /** Blows away the stored cache values. */
547 public void clearCache() {
548 mInfoMap.clear();
549 mCallBacks.clear();
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700550 mQueryId = 0;
Eric Erfanianccca3152017-02-22 16:32:36 -0800551 }
552
553 private ContactCacheEntry buildEntry(
554 Context context, CallerInfo info, int presentation, boolean isIncoming) {
555 final ContactCacheEntry cce = new ContactCacheEntry();
556 populateCacheEntry(context, info, cce, presentation, isIncoming);
557
558 // This will only be true for emergency numbers
559 if (info.photoResource != 0) {
560 cce.photo = context.getResources().getDrawable(info.photoResource);
561 } else if (info.isCachedPhotoCurrent) {
562 if (info.cachedPhoto != null) {
563 cce.photo = info.cachedPhoto;
564 cce.photoType = ContactPhotoType.CONTACT;
565 } else {
566 cce.photo = getDefaultContactPhotoDrawable();
567 cce.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
568 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800569 } else {
570 cce.displayPhotoUri = info.contactDisplayPhotoUri;
571 cce.photo = null;
572 }
573
574 // Support any contact id in N because QuickContacts in N starts supporting enterprise
575 // contact id
576 if (info.lookupKeyOrNull != null
577 && (VERSION.SDK_INT >= VERSION_CODES.N || info.contactIdOrZero != 0)) {
578 cce.lookupUri = Contacts.getLookupUri(info.contactIdOrZero, info.lookupKeyOrNull);
579 } else {
580 Log.v(TAG, "lookup key is null or contact ID is 0 on M. Don't create a lookup uri.");
581 cce.lookupUri = null;
582 }
583
584 cce.lookupKey = info.lookupKeyOrNull;
585 cce.contactRingtoneUri = info.contactRingtoneUri;
586 if (cce.contactRingtoneUri == null || Uri.EMPTY.equals(cce.contactRingtoneUri)) {
587 cce.contactRingtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
588 }
589
590 return cce;
591 }
592
593 /** Sends the updated information to call the callbacks for the entry. */
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700594 @MainThread
Eric Erfanianccca3152017-02-22 16:32:36 -0800595 private void sendInfoNotifications(String callId, ContactCacheEntry entry) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700596 Assert.isMainThread();
Eric Erfanianccca3152017-02-22 16:32:36 -0800597 final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
598 if (callBacks != null) {
599 for (ContactInfoCacheCallback callBack : callBacks) {
600 callBack.onContactInfoComplete(callId, entry);
601 }
602 }
603 }
604
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700605 @MainThread
Eric Erfanianccca3152017-02-22 16:32:36 -0800606 private void sendImageNotifications(String callId, ContactCacheEntry entry) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700607 Assert.isMainThread();
Eric Erfanianccca3152017-02-22 16:32:36 -0800608 final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
609 if (callBacks != null && entry.photo != null) {
610 for (ContactInfoCacheCallback callBack : callBacks) {
611 callBack.onImageLoadComplete(callId, entry);
612 }
613 }
614 }
615
616 private void clearCallbacks(String callId) {
617 mCallBacks.remove(callId);
618 }
619
620 public Drawable getDefaultContactPhotoDrawable() {
621 if (mDefaultContactPhotoDrawable == null) {
622 mDefaultContactPhotoDrawable =
623 mContext.getResources().getDrawable(R.drawable.img_no_image_automirrored);
624 }
625 return mDefaultContactPhotoDrawable;
626 }
627
628 public Drawable getConferenceDrawable() {
629 if (mConferencePhotoDrawable == null) {
630 mConferencePhotoDrawable =
631 mContext.getResources().getDrawable(R.drawable.img_conference_automirrored);
632 }
633 return mConferencePhotoDrawable;
634 }
635
636 /** Callback interface for the contact query. */
637 public interface ContactInfoCacheCallback {
638
639 void onContactInfoComplete(String callId, ContactCacheEntry entry);
640
641 void onImageLoadComplete(String callId, ContactCacheEntry entry);
642 }
643
644 /** This is cached contact info, which should be the ONLY info used by UI. */
645 public static class ContactCacheEntry {
646
647 public String namePrimary;
648 public String nameAlternative;
649 public String number;
650 public String location;
651 public String label;
652 public Drawable photo;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700653 @ContactPhotoType int photoType;
654 boolean isSipCall;
Eric Erfanianccca3152017-02-22 16:32:36 -0800655 // Note in cache entry whether this is a pending async loading action to know whether to
656 // wait for its callback or not.
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700657 boolean hasPendingQuery;
Eric Erfanianccca3152017-02-22 16:32:36 -0800658 /** This will be used for the "view" notification. */
659 public Uri contactUri;
660 /** Either a display photo or a thumbnail URI. */
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700661 Uri displayPhotoUri;
Eric Erfanianccca3152017-02-22 16:32:36 -0800662
663 public Uri lookupUri; // Sent to NotificationMananger
664 public String lookupKey;
665 public int contactLookupResult = ContactLookupResult.Type.NOT_FOUND;
666 public long userType = ContactsUtils.USER_TYPE_CURRENT;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700667 Uri contactRingtoneUri;
668 /** Query id to identify the query session. */
669 int queryId;
670 /** The phone number without any changes to display to the user (ex: cnap...) */
671 String originalPhoneNumber;
672 boolean isBusiness;
Eric Erfanianccca3152017-02-22 16:32:36 -0800673
674 @Override
675 public String toString() {
676 return "ContactCacheEntry{"
677 + "name='"
678 + MoreStrings.toSafeString(namePrimary)
679 + '\''
680 + ", nameAlternative='"
681 + MoreStrings.toSafeString(nameAlternative)
682 + '\''
683 + ", number='"
684 + MoreStrings.toSafeString(number)
685 + '\''
686 + ", location='"
687 + MoreStrings.toSafeString(location)
688 + '\''
689 + ", label='"
690 + label
691 + '\''
692 + ", photo="
693 + photo
694 + ", isSipCall="
695 + isSipCall
696 + ", contactUri="
697 + contactUri
698 + ", displayPhotoUri="
699 + displayPhotoUri
700 + ", contactLookupResult="
701 + contactLookupResult
702 + ", userType="
703 + userType
704 + ", contactRingtoneUri="
705 + contactRingtoneUri
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700706 + ", queryId="
707 + queryId
708 + ", originalPhoneNumber="
709 + originalPhoneNumber
Eric Erfanianccca3152017-02-22 16:32:36 -0800710 + '}';
711 }
712 }
713
714 private static final class DialerCallCookieWrapper {
715 public final String callId;
716 public final int numberPresentation;
717
718 public DialerCallCookieWrapper(String callId, int numberPresentation) {
719 this.callId = callId;
720 this.numberPresentation = numberPresentation;
721 }
722 }
723
724 private class FindInfoCallback implements OnQueryCompleteListener {
725
726 private final boolean mIsIncoming;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700727 private final CallerInfoQueryToken mQueryToken;
Eric Erfanianccca3152017-02-22 16:32:36 -0800728
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700729 public FindInfoCallback(boolean isIncoming, CallerInfoQueryToken queryToken) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800730 mIsIncoming = isIncoming;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700731 mQueryToken = queryToken;
Eric Erfanianccca3152017-02-22 16:32:36 -0800732 }
733
734 @Override
735 public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
736 Assert.isWorkerThread();
737 DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700738 if (!isWaitingForThisQuery(cw.callId, mQueryToken.mQueryId)) {
739 return;
740 }
741 updateCallerInfoInCacheOnAnyThread(
742 cw.callId, cw.numberPresentation, ci, mIsIncoming, true, mQueryToken);
Eric Erfanianccca3152017-02-22 16:32:36 -0800743 }
744
745 @Override
746 public void onQueryComplete(int token, Object cookie, CallerInfo callerInfo) {
747 Assert.isMainThread();
748 DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie;
749 String callId = cw.callId;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700750 if (!isWaitingForThisQuery(cw.callId, mQueryToken.mQueryId)) {
751 return;
752 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800753 ContactCacheEntry cacheEntry = mInfoMap.get(callId);
754 // This may happen only when InCallPresenter attempt to cleanup.
755 if (cacheEntry == null) {
756 Log.w(TAG, "Contact lookup done, but cache entry is not found.");
757 clearCallbacks(callId);
758 return;
759 }
760 sendInfoNotifications(callId, cacheEntry);
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700761 if (!cacheEntry.hasPendingQuery) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800762 if (callerInfo.contactExists) {
763 Log.d(TAG, "Contact lookup done. Local contact found, no image.");
764 } else {
765 Log.d(
766 TAG,
767 "Contact lookup done. Local contact not found and"
768 + " no remote lookup service available.");
769 }
770 clearCallbacks(callId);
771 }
772 }
773 }
774
775 class PhoneNumberServiceListener
776 implements PhoneNumberService.NumberLookupListener, PhoneNumberService.ImageLookupListener {
777
778 private final String mCallId;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700779 private final int mQueryIdOfRemoteLookup;
Eric Erfanianccca3152017-02-22 16:32:36 -0800780
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700781 PhoneNumberServiceListener(String callId, int queryId) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800782 mCallId = callId;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700783 mQueryIdOfRemoteLookup = queryId;
Eric Erfanianccca3152017-02-22 16:32:36 -0800784 }
785
786 @Override
787 public void onPhoneNumberInfoComplete(final PhoneNumberService.PhoneNumberInfo info) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700788 Log.d(TAG, "PhoneNumberServiceListener.onPhoneNumberInfoComplete");
789 if (!isWaitingForThisQuery(mCallId, mQueryIdOfRemoteLookup)) {
790 return;
791 }
792
Eric Erfanianccca3152017-02-22 16:32:36 -0800793 // If we got a miss, this is the end of the lookup pipeline,
794 // so clear the callbacks and return.
795 if (info == null) {
796 Log.d(TAG, "Contact lookup done. Remote contact not found.");
797 clearCallbacks(mCallId);
798 return;
799 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800800 ContactCacheEntry entry = new ContactCacheEntry();
801 entry.namePrimary = info.getDisplayName();
802 entry.number = info.getNumber();
803 entry.contactLookupResult = info.getLookupSource();
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700804 entry.isBusiness = info.isBusiness();
Eric Erfanianccca3152017-02-22 16:32:36 -0800805 final int type = info.getPhoneType();
806 final String label = info.getPhoneLabel();
807 if (type == Phone.TYPE_CUSTOM) {
808 entry.label = label;
809 } else {
810 final CharSequence typeStr = Phone.getTypeLabel(mContext.getResources(), type, label);
811 entry.label = typeStr == null ? null : typeStr.toString();
812 }
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700813 final ContactCacheEntry oldEntry = mInfoMap.get(mCallId);
814 if (oldEntry != null) {
815 // Location is only obtained from local lookup so persist
816 // the value for remote lookups. Once we have a name this
817 // field is no longer used; it is persisted here in case
818 // the UI is ever changed to use it.
819 entry.location = oldEntry.location;
820 // Contact specific ringtone is obtained from local lookup.
821 entry.contactRingtoneUri = oldEntry.contactRingtoneUri;
Eric Erfanianccca3152017-02-22 16:32:36 -0800822 }
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700823
824 // If no image and it's a business, switch to using the default business avatar.
825 if (info.getImageUrl() == null && info.isBusiness()) {
826 Log.d(TAG, "Business has no image. Using default.");
827 entry.photo = mContext.getResources().getDrawable(R.drawable.img_business);
828 entry.photoType = ContactPhotoType.BUSINESS;
829 }
830
831 Log.d(TAG, "put entry into map: " + entry);
832 mInfoMap.put(mCallId, entry);
Eric Erfanianccca3152017-02-22 16:32:36 -0800833 sendInfoNotifications(mCallId, entry);
834
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700835 entry.hasPendingQuery = info.getImageUrl() != null;
Eric Erfanianccca3152017-02-22 16:32:36 -0800836
837 // If there is no image then we should not expect another callback.
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700838 if (!entry.hasPendingQuery) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800839 // We're done, so clear callbacks
840 clearCallbacks(mCallId);
841 }
842 }
843
844 @Override
845 public void onImageFetchComplete(Bitmap bitmap) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700846 Log.d(TAG, "PhoneNumberServiceListener.onImageFetchComplete");
847 if (!isWaitingForThisQuery(mCallId, mQueryIdOfRemoteLookup)) {
848 return;
849 }
850 CallerInfoQueryToken queryToken = new CallerInfoQueryToken(mQueryIdOfRemoteLookup, mCallId);
851 loadImage(null, bitmap, queryToken);
852 onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, queryToken);
853 }
854 }
855
856 private boolean needForceQuery(DialerCall call, ContactCacheEntry cacheEntry) {
857 if (call == null || call.isConferenceCall()) {
858 return false;
859 }
860
861 String newPhoneNumber = PhoneNumberUtils.stripSeparators(call.getNumber());
862 if (cacheEntry == null) {
863 // No info in the map yet so it is the 1st query
864 Log.d(TAG, "needForceQuery: first query");
865 return true;
866 }
867 String oldPhoneNumber = PhoneNumberUtils.stripSeparators(cacheEntry.originalPhoneNumber);
868
869 if (!TextUtils.equals(oldPhoneNumber, newPhoneNumber)) {
870 Log.d(TAG, "phone number has changed: " + oldPhoneNumber + " -> " + newPhoneNumber);
871 return true;
872 }
873
874 return false;
875 }
876
877 private static final class CallerInfoQueryToken {
878 final int mQueryId;
879 final String mCallId;
880
881 CallerInfoQueryToken(int queryId, String callId) {
882 mQueryId = queryId;
883 mCallId = callId;
884 }
885 }
886
887 /** Check if the queryId in the cached map is the same as the one from query result. */
888 private boolean isWaitingForThisQuery(String callId, int queryId) {
889 final ContactCacheEntry existingCacheEntry = mInfoMap.get(callId);
890 if (existingCacheEntry == null) {
891 // This might happen if lookup on background thread comes back before the initial entry is
892 // created.
893 Log.d(TAG, "Cached entry is null.");
894 return true;
895 } else {
896 int waitingQueryId = existingCacheEntry.queryId;
897 Log.d(TAG, "waitingQueryId = " + waitingQueryId + "; queryId = " + queryId);
898 return waitingQueryId == queryId;
Eric Erfanianccca3152017-02-22 16:32:36 -0800899 }
900 }
901}