blob: 335895220398bf1019c88dc4585fbab6f81b1f4a [file] [log] [blame]
Eric Erfanianccca3152017-02-22 16:32:36 -08001/*
2 * Copyright (C) 2006 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.Manifest;
20import android.annotation.TargetApi;
21import android.content.AsyncQueryHandler;
22import android.content.ContentResolver;
23import android.content.Context;
24import android.database.Cursor;
25import android.database.SQLException;
26import android.net.Uri;
27import android.os.Build.VERSION;
28import android.os.Build.VERSION_CODES;
29import android.os.Handler;
30import android.os.Looper;
31import android.os.Message;
wangqi9982f0d2017-10-11 17:46:07 -070032import android.os.Trace;
Eric Erfanianccca3152017-02-22 16:32:36 -080033import android.provider.ContactsContract;
34import android.provider.ContactsContract.Directory;
35import android.support.annotation.MainThread;
36import android.support.annotation.RequiresPermission;
37import android.support.annotation.WorkerThread;
38import android.telephony.PhoneNumberUtils;
39import android.text.TextUtils;
linyuhc3d3c3d2018-02-27 13:42:59 -080040import com.android.dialer.common.cp2.DirectoryCompat;
Eric Erfanianccca3152017-02-22 16:32:36 -080041import com.android.dialer.phonenumbercache.CachedNumberLookupService;
42import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
43import com.android.dialer.phonenumbercache.ContactInfoHelper;
44import com.android.dialer.phonenumbercache.PhoneNumberCache;
wangqie37d60c2017-09-27 10:13:49 -070045import com.android.dialer.strictmode.StrictModeUtils;
Eric Erfanianccca3152017-02-22 16:32:36 -080046import java.io.IOException;
47import java.io.InputStream;
48import java.util.ArrayList;
49import java.util.Arrays;
50
51/**
52 * Helper class to make it easier to run asynchronous caller-id lookup queries.
53 *
54 * @see CallerInfo
55 */
56@TargetApi(VERSION_CODES.M)
57public class CallerInfoAsyncQuery {
58
59 /** Interface for a CallerInfoAsyncQueryHandler result return. */
Eric Erfaniand5e47f62017-03-15 14:41:07 -070060 interface OnQueryCompleteListener {
Eric Erfanianccca3152017-02-22 16:32:36 -080061
62 /** Called when the query is complete. */
63 @MainThread
64 void onQueryComplete(int token, Object cookie, CallerInfo ci);
65
66 /** Called when data is loaded. Must be called in worker thread. */
67 @WorkerThread
68 void onDataLoaded(int token, Object cookie, CallerInfo ci);
69 }
70
71 private static final boolean DBG = false;
72 private static final String LOG_TAG = "CallerInfoAsyncQuery";
73
74 private static final int EVENT_NEW_QUERY = 1;
75 private static final int EVENT_ADD_LISTENER = 2;
76 private static final int EVENT_EMERGENCY_NUMBER = 3;
77 private static final int EVENT_VOICEMAIL_NUMBER = 4;
78 // If the CallerInfo query finds no contacts, should we use the
79 // PhoneNumberOfflineGeocoder to look up a "geo description"?
80 // (TODO: This could become a flag in config.xml if it ever needs to be
81 // configured on a per-product basis.)
82 private static final boolean ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION = true;
83 /* Directory lookup related code - START */
84 private static final String[] DIRECTORY_PROJECTION = new String[] {Directory._ID};
85
86 /** Private constructor for factory methods. */
87 private CallerInfoAsyncQuery() {}
88
89 @RequiresPermission(Manifest.permission.READ_CONTACTS)
Eric Erfaniand5e47f62017-03-15 14:41:07 -070090 static void startQuery(
Eric Erfanianccca3152017-02-22 16:32:36 -080091 final int token,
92 final Context context,
93 final CallerInfo info,
94 final OnQueryCompleteListener listener,
95 final Object cookie) {
96 Log.d(LOG_TAG, "##### CallerInfoAsyncQuery startContactProviderQuery()... #####");
97 Log.d(LOG_TAG, "- number: " + info.phoneNumber);
98 Log.d(LOG_TAG, "- cookie: " + cookie);
99
100 OnQueryCompleteListener contactsProviderQueryCompleteListener =
101 new OnQueryCompleteListener() {
102 @Override
103 public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700104 Log.d(LOG_TAG, "contactsProviderQueryCompleteListener onQueryComplete");
Eric Erfanianccca3152017-02-22 16:32:36 -0800105 // If there are no other directory queries, make sure that the listener is
Eric Erfanian938468d2017-10-24 14:05:52 -0700106 // notified of this result. see a bug
Eric Erfanianccca3152017-02-22 16:32:36 -0800107 if ((ci != null && ci.contactExists)
108 || !startOtherDirectoriesQuery(token, context, info, listener, cookie)) {
109 if (listener != null && ci != null) {
110 listener.onQueryComplete(token, cookie, ci);
111 }
112 }
113 }
114
115 @Override
116 public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700117 Log.d(LOG_TAG, "contactsProviderQueryCompleteListener onDataLoaded");
Eric Erfanianccca3152017-02-22 16:32:36 -0800118 listener.onDataLoaded(token, cookie, ci);
119 }
120 };
121 startDefaultDirectoryQuery(token, context, info, contactsProviderQueryCompleteListener, cookie);
122 }
123
124 // Private methods
125 private static void startDefaultDirectoryQuery(
126 int token,
127 Context context,
128 CallerInfo info,
129 OnQueryCompleteListener listener,
130 Object cookie) {
131 // Construct the URI object and query params, and start the query.
132 Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber);
133 startQueryInternal(token, context, info, listener, cookie, uri);
134 }
135
136 /**
137 * Factory method to start the query based on a CallerInfo object.
138 *
139 * <p>Note: if the number contains an "@" character we treat it as a SIP address, and look it up
140 * directly in the Data table rather than using the PhoneLookup table. TODO: But eventually we
141 * should expose two separate methods, one for numbers and one for SIP addresses, and then have
142 * PhoneUtils.startGetCallerInfo() decide which one to call based on the phone type of the
143 * incoming connection.
144 */
145 private static void startQueryInternal(
146 int token,
147 Context context,
148 CallerInfo info,
149 OnQueryCompleteListener listener,
150 Object cookie,
151 Uri contactRef) {
152 if (DBG) {
153 Log.d(LOG_TAG, "==> contactRef: " + sanitizeUriToString(contactRef));
154 }
155
156 if ((context == null) || (contactRef == null)) {
157 throw new QueryPoolException("Bad context or query uri.");
158 }
159 CallerInfoAsyncQueryHandler handler = new CallerInfoAsyncQueryHandler(context, contactRef);
160
161 //create cookieWrapper, start query
162 CookieWrapper cw = new CookieWrapper();
163 cw.listener = listener;
164 cw.cookie = cookie;
165 cw.number = info.phoneNumber;
wangqi1420a222017-09-21 09:37:40 -0700166 cw.countryIso = info.countryIso;
Eric Erfanianccca3152017-02-22 16:32:36 -0800167
168 // check to see if these are recognized numbers, and use shortcuts if we can.
169 if (PhoneNumberUtils.isLocalEmergencyNumber(context, info.phoneNumber)) {
170 cw.event = EVENT_EMERGENCY_NUMBER;
171 } else if (info.isVoiceMailNumber()) {
172 cw.event = EVENT_VOICEMAIL_NUMBER;
173 } else {
174 cw.event = EVENT_NEW_QUERY;
175 }
176
177 String[] proejection = CallerInfo.getDefaultPhoneLookupProjection(contactRef);
178 handler.startQuery(
179 token,
180 cw, // cookie
181 contactRef, // uri
182 proejection, // projection
183 null, // selection
184 null, // selectionArgs
185 null); // orderBy
186 }
187
188 // Return value indicates if listener was notified.
189 private static boolean startOtherDirectoriesQuery(
190 int token,
191 Context context,
192 CallerInfo info,
193 OnQueryCompleteListener listener,
194 Object cookie) {
wangqi9982f0d2017-10-11 17:46:07 -0700195 Trace.beginSection("CallerInfoAsyncQuery.startOtherDirectoriesQuery");
wangqie37d60c2017-09-27 10:13:49 -0700196 long[] directoryIds = StrictModeUtils.bypass(() -> getDirectoryIds(context));
Eric Erfanianccca3152017-02-22 16:32:36 -0800197 int size = directoryIds.length;
198 if (size == 0) {
wangqi9982f0d2017-10-11 17:46:07 -0700199 Trace.endSection();
Eric Erfanianccca3152017-02-22 16:32:36 -0800200 return false;
201 }
202
203 DirectoryQueryCompleteListenerFactory listenerFactory =
204 new DirectoryQueryCompleteListenerFactory(context, size, listener);
205
206 // The current implementation of multiple async query runs in single handler thread
207 // in AsyncQueryHandler.
208 // intermediateListener.onQueryComplete is also called from the same caller thread.
Eric Erfanian938468d2017-10-24 14:05:52 -0700209 // TODO(a bug): use thread pool instead of single thread.
Eric Erfanianccca3152017-02-22 16:32:36 -0800210 for (int i = 0; i < size; i++) {
211 long directoryId = directoryIds[i];
212 Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber, directoryId);
213 if (DBG) {
214 Log.d(LOG_TAG, "directoryId: " + directoryId + " uri: " + uri);
215 }
216 OnQueryCompleteListener intermediateListener = listenerFactory.newListener(directoryId);
217 startQueryInternal(token, context, info, intermediateListener, cookie, uri);
218 }
wangqi9982f0d2017-10-11 17:46:07 -0700219 Trace.endSection();
Eric Erfanianccca3152017-02-22 16:32:36 -0800220 return true;
221 }
222
223 private static long[] getDirectoryIds(Context context) {
224 ArrayList<Long> results = new ArrayList<>();
225
226 Uri uri = Directory.CONTENT_URI;
227 if (VERSION.SDK_INT >= VERSION_CODES.N) {
228 uri = Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "directories_enterprise");
229 }
230
231 ContentResolver cr = context.getContentResolver();
232 Cursor cursor = cr.query(uri, DIRECTORY_PROJECTION, null, null, null);
233 addDirectoryIdsFromCursor(cursor, results);
234
235 long[] result = new long[results.size()];
236 for (int i = 0; i < results.size(); i++) {
237 result[i] = results.get(i);
238 }
239 return result;
240 }
241
242 private static void addDirectoryIdsFromCursor(Cursor cursor, ArrayList<Long> results) {
243 if (cursor != null) {
244 int idIndex = cursor.getColumnIndex(Directory._ID);
245 while (cursor.moveToNext()) {
246 long id = cursor.getLong(idIndex);
247 if (DirectoryCompat.isRemoteDirectoryId(id)) {
248 results.add(id);
249 }
250 }
251 cursor.close();
252 }
253 }
254
255 private static String sanitizeUriToString(Uri uri) {
256 if (uri != null) {
257 String uriString = uri.toString();
258 int indexOfLastSlash = uriString.lastIndexOf('/');
259 if (indexOfLastSlash > 0) {
260 return uriString.substring(0, indexOfLastSlash) + "/xxxxxxx";
261 } else {
262 return uriString;
263 }
264 } else {
265 return "";
266 }
267 }
268
269 /** Wrap the cookie from the WorkerArgs with additional information needed by our classes. */
270 private static final class CookieWrapper {
271
272 public OnQueryCompleteListener listener;
273 public Object cookie;
274 public int event;
275 public String number;
wangqi1420a222017-09-21 09:37:40 -0700276 public String countryIso;
Eric Erfanianccca3152017-02-22 16:32:36 -0800277 }
278 /* Directory lookup related code - END */
279
280 /** Simple exception used to communicate problems with the query pool. */
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700281 private static class QueryPoolException extends SQLException {
Eric Erfanianccca3152017-02-22 16:32:36 -0800282
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700283 QueryPoolException(String error) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800284 super(error);
285 }
286 }
287
288 private static final class DirectoryQueryCompleteListenerFactory {
289
linyuh183cb712017-12-27 17:02:37 -0800290 private final OnQueryCompleteListener listener;
291 private final Context context;
Eric Erfanianccca3152017-02-22 16:32:36 -0800292 // Make sure listener to be called once and only once
linyuh183cb712017-12-27 17:02:37 -0800293 private int count;
294 private boolean isListenerCalled;
Eric Erfanianccca3152017-02-22 16:32:36 -0800295
296 DirectoryQueryCompleteListenerFactory(
297 Context context, int size, OnQueryCompleteListener listener) {
linyuh183cb712017-12-27 17:02:37 -0800298 count = size;
299 this.listener = listener;
300 isListenerCalled = false;
301 this.context = context;
Eric Erfanianccca3152017-02-22 16:32:36 -0800302 }
303
304 private void onDirectoryQueryComplete(
305 int token, Object cookie, CallerInfo ci, long directoryId) {
306 boolean shouldCallListener = false;
307 synchronized (this) {
linyuh183cb712017-12-27 17:02:37 -0800308 count = count - 1;
309 if (!isListenerCalled && (ci.contactExists || count == 0)) {
310 isListenerCalled = true;
Eric Erfanianccca3152017-02-22 16:32:36 -0800311 shouldCallListener = true;
312 }
313 }
314
315 // Don't call callback in synchronized block because mListener.onQueryComplete may
316 // take long time to complete
linyuh183cb712017-12-27 17:02:37 -0800317 if (shouldCallListener && listener != null) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800318 addCallerInfoIntoCache(ci, directoryId);
linyuh183cb712017-12-27 17:02:37 -0800319 listener.onQueryComplete(token, cookie, ci);
Eric Erfanianccca3152017-02-22 16:32:36 -0800320 }
321 }
322
323 private void addCallerInfoIntoCache(CallerInfo ci, long directoryId) {
324 CachedNumberLookupService cachedNumberLookupService =
linyuh183cb712017-12-27 17:02:37 -0800325 PhoneNumberCache.get(context).getCachedNumberLookupService();
Eric Erfanianccca3152017-02-22 16:32:36 -0800326 if (ci.contactExists && cachedNumberLookupService != null) {
327 // 1. Cache caller info
328 CachedContactInfo cachedContactInfo =
329 CallerInfoUtils.buildCachedContactInfo(cachedNumberLookupService, ci);
linyuh183cb712017-12-27 17:02:37 -0800330 String directoryLabel = context.getString(R.string.directory_search_label);
Eric Erfanianccca3152017-02-22 16:32:36 -0800331 cachedContactInfo.setDirectorySource(directoryLabel, directoryId);
linyuh183cb712017-12-27 17:02:37 -0800332 cachedNumberLookupService.addContact(context, cachedContactInfo);
Eric Erfanianccca3152017-02-22 16:32:36 -0800333
334 // 2. Cache photo
335 if (ci.contactDisplayPhotoUri != null && ci.normalizedNumber != null) {
336 try (InputStream in =
linyuh183cb712017-12-27 17:02:37 -0800337 context.getContentResolver().openInputStream(ci.contactDisplayPhotoUri)) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800338 if (in != null) {
linyuh183cb712017-12-27 17:02:37 -0800339 cachedNumberLookupService.addPhoto(context, ci.normalizedNumber, in);
Eric Erfanianccca3152017-02-22 16:32:36 -0800340 }
341 } catch (IOException e) {
342 Log.e(LOG_TAG, "failed to fetch directory contact photo", e);
343 }
344 }
345 }
346 }
347
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700348 OnQueryCompleteListener newListener(long directoryId) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800349 return new DirectoryQueryCompleteListener(directoryId);
350 }
351
352 private class DirectoryQueryCompleteListener implements OnQueryCompleteListener {
353
linyuh183cb712017-12-27 17:02:37 -0800354 private final long directoryId;
Eric Erfanianccca3152017-02-22 16:32:36 -0800355
356 DirectoryQueryCompleteListener(long directoryId) {
linyuh183cb712017-12-27 17:02:37 -0800357 this.directoryId = directoryId;
Eric Erfanianccca3152017-02-22 16:32:36 -0800358 }
359
360 @Override
361 public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700362 Log.d(LOG_TAG, "DirectoryQueryCompleteListener.onDataLoaded");
linyuh183cb712017-12-27 17:02:37 -0800363 listener.onDataLoaded(token, cookie, ci);
Eric Erfanianccca3152017-02-22 16:32:36 -0800364 }
365
366 @Override
367 public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700368 Log.d(LOG_TAG, "DirectoryQueryCompleteListener.onQueryComplete");
linyuh183cb712017-12-27 17:02:37 -0800369 onDirectoryQueryComplete(token, cookie, ci, directoryId);
Eric Erfanianccca3152017-02-22 16:32:36 -0800370 }
371 }
372 }
373
374 /** Our own implementation of the AsyncQueryHandler. */
375 private static class CallerInfoAsyncQueryHandler extends AsyncQueryHandler {
376
377 /**
378 * The information relevant to each CallerInfo query. Each query may have multiple listeners, so
379 * each AsyncCursorInfo is associated with 2 or more CookieWrapper objects in the queue (one
380 * with a new query event, and one with a end event, with 0 or more additional listeners in
381 * between).
382 */
linyuh183cb712017-12-27 17:02:37 -0800383 private Context queryContext;
Eric Erfanianccca3152017-02-22 16:32:36 -0800384
linyuh183cb712017-12-27 17:02:37 -0800385 private Uri queryUri;
386 private CallerInfo callerInfo;
Eric Erfanianccca3152017-02-22 16:32:36 -0800387
388 /** Asynchronous query handler class for the contact / callerinfo object. */
389 private CallerInfoAsyncQueryHandler(Context context, Uri contactRef) {
390 super(context.getContentResolver());
linyuh183cb712017-12-27 17:02:37 -0800391 this.queryContext = context;
392 this.queryUri = contactRef;
Eric Erfanianccca3152017-02-22 16:32:36 -0800393 }
394
395 @Override
396 public void startQuery(
397 int token,
398 Object cookie,
399 Uri uri,
400 String[] projection,
401 String selection,
402 String[] selectionArgs,
403 String orderBy) {
404 if (DBG) {
405 // Show stack trace with the arguments.
406 Log.d(
407 LOG_TAG,
408 "InCall: startQuery: url="
409 + uri
410 + " projection=["
411 + Arrays.toString(projection)
412 + "]"
413 + " selection="
414 + selection
415 + " "
416 + " args=["
417 + Arrays.toString(selectionArgs)
418 + "]",
419 new RuntimeException("STACKTRACE"));
420 }
421 super.startQuery(token, cookie, uri, projection, selection, selectionArgs, orderBy);
422 }
423
424 @Override
425 protected Handler createHandler(Looper looper) {
426 return new CallerInfoWorkerHandler(looper);
427 }
428
429 /**
430 * Overrides onQueryComplete from AsyncQueryHandler.
431 *
432 * <p>This method takes into account the state of this class; we construct the CallerInfo object
433 * only once for each set of listeners. When the query thread has done its work and calls this
434 * method, we inform the remaining listeners in the queue, until we're out of listeners. Once we
435 * get the message indicating that we should expect no new listeners for this CallerInfo object,
436 * we release the AsyncCursorInfo back into the pool.
437 */
438 @Override
439 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
440 Log.d(this, "##### onQueryComplete() ##### query complete for token: " + token);
441
442 CookieWrapper cw = (CookieWrapper) cookie;
443
444 if (cw.listener != null) {
445 Log.d(
446 this,
447 "notifying listener: "
448 + cw.listener.getClass().toString()
449 + " for token: "
450 + token
linyuh183cb712017-12-27 17:02:37 -0800451 + callerInfo);
452 cw.listener.onQueryComplete(token, cw.cookie, callerInfo);
Eric Erfanianccca3152017-02-22 16:32:36 -0800453 }
linyuh183cb712017-12-27 17:02:37 -0800454 queryContext = null;
455 queryUri = null;
456 callerInfo = null;
Eric Erfanianccca3152017-02-22 16:32:36 -0800457 }
458
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700459 void updateData(int token, Object cookie, Cursor cursor) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800460 try {
461 Log.d(this, "##### updateData() ##### for token: " + token);
462
463 //get the cookie and notify the listener.
464 CookieWrapper cw = (CookieWrapper) cookie;
465 if (cw == null) {
466 // Normally, this should never be the case for calls originating
467 // from within this code.
468 // However, if there is any code that calls this method, we should
469 // check the parameters to make sure they're viable.
470 Log.d(this, "Cookie is null, ignoring onQueryComplete() request.");
471 return;
472 }
473
474 // check the token and if needed, create the callerinfo object.
linyuh183cb712017-12-27 17:02:37 -0800475 if (callerInfo == null) {
476 if ((queryContext == null) || (queryUri == null)) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800477 throw new QueryPoolException(
478 "Bad context or query uri, or CallerInfoAsyncQuery already released.");
479 }
480
481 // adjust the callerInfo data as needed, and only if it was set from the
482 // initial query request.
483 // Change the callerInfo number ONLY if it is an emergency number or the
484 // voicemail number, and adjust other data (including photoResource)
485 // accordingly.
486 if (cw.event == EVENT_EMERGENCY_NUMBER) {
487 // Note we're setting the phone number here (refer to javadoc
488 // comments at the top of CallerInfo class).
linyuh183cb712017-12-27 17:02:37 -0800489 callerInfo = new CallerInfo().markAsEmergency(queryContext);
Eric Erfanianccca3152017-02-22 16:32:36 -0800490 } else if (cw.event == EVENT_VOICEMAIL_NUMBER) {
linyuh183cb712017-12-27 17:02:37 -0800491 callerInfo = new CallerInfo().markAsVoiceMail(queryContext);
Eric Erfanianccca3152017-02-22 16:32:36 -0800492 } else {
linyuh183cb712017-12-27 17:02:37 -0800493 callerInfo = CallerInfo.getCallerInfo(queryContext, queryUri, cursor);
494 Log.d(this, "==> Got mCallerInfo: " + callerInfo);
Eric Erfanianccca3152017-02-22 16:32:36 -0800495
496 CallerInfo newCallerInfo =
linyuh183cb712017-12-27 17:02:37 -0800497 CallerInfo.doSecondaryLookupIfNecessary(queryContext, cw.number, callerInfo);
498 if (newCallerInfo != callerInfo) {
499 callerInfo = newCallerInfo;
500 Log.d(this, "#####async contact look up with numeric username" + callerInfo);
Eric Erfanianccca3152017-02-22 16:32:36 -0800501 }
linyuh183cb712017-12-27 17:02:37 -0800502 callerInfo.countryIso = cw.countryIso;
Eric Erfanianccca3152017-02-22 16:32:36 -0800503
504 // Final step: look up the geocoded description.
505 if (ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION) {
506 // Note we do this only if we *don't* have a valid name (i.e. if
507 // no contacts matched the phone number of the incoming call),
508 // since that's the only case where the incoming-call UI cares
509 // about this field.
510 //
511 // (TODO: But if we ever want the UI to show the geoDescription
512 // even when we *do* match a contact, we'll need to either call
513 // updateGeoDescription() unconditionally here, or possibly add a
514 // new parameter to CallerInfoAsyncQuery.startQuery() to force
515 // the geoDescription field to be populated.)
516
linyuh183cb712017-12-27 17:02:37 -0800517 if (TextUtils.isEmpty(callerInfo.name)) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800518 // Actually when no contacts match the incoming phone number,
519 // the CallerInfo object is totally blank here (i.e. no name
520 // *or* phoneNumber). So we need to pass in cw.number as
521 // a fallback number.
linyuh183cb712017-12-27 17:02:37 -0800522 callerInfo.updateGeoDescription(queryContext, cw.number);
Eric Erfanianccca3152017-02-22 16:32:36 -0800523 }
524 }
525
526 // Use the number entered by the user for display.
527 if (!TextUtils.isEmpty(cw.number)) {
linyuh183cb712017-12-27 17:02:37 -0800528 callerInfo.phoneNumber = cw.number;
Eric Erfanianccca3152017-02-22 16:32:36 -0800529 }
530 }
531
532 Log.d(this, "constructing CallerInfo object for token: " + token);
533
534 if (cw.listener != null) {
linyuh183cb712017-12-27 17:02:37 -0800535 cw.listener.onDataLoaded(token, cw.cookie, callerInfo);
Eric Erfanianccca3152017-02-22 16:32:36 -0800536 }
537 }
538
539 } finally {
540 // The cursor may have been closed in CallerInfo.getCallerInfo()
541 if (cursor != null && !cursor.isClosed()) {
542 cursor.close();
543 }
544 }
545 }
546
547 /**
548 * Our own query worker thread.
549 *
550 * <p>This thread handles the messages enqueued in the looper. The normal sequence of events is
551 * that a new query shows up in the looper queue, followed by 0 or more add listener requests,
552 * and then an end request. Of course, these requests can be interlaced with requests from other
553 * tokens, but is irrelevant to this handler since the handler has no state.
554 *
555 * <p>Note that we depend on the queue to keep things in order; in other words, the looper queue
556 * must be FIFO with respect to input from the synchronous startQuery calls and output to this
557 * handleMessage call.
558 *
559 * <p>This use of the queue is required because CallerInfo objects may be accessed multiple
560 * times before the query is complete. All accesses (listeners) must be queued up and informed
561 * in order when the query is complete.
562 */
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700563 class CallerInfoWorkerHandler extends WorkerHandler {
Eric Erfanianccca3152017-02-22 16:32:36 -0800564
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700565 CallerInfoWorkerHandler(Looper looper) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800566 super(looper);
567 }
568
569 @Override
570 public void handleMessage(Message msg) {
571 WorkerArgs args = (WorkerArgs) msg.obj;
572 CookieWrapper cw = (CookieWrapper) args.cookie;
573
574 if (cw == null) {
575 // Normally, this should never be the case for calls originating
576 // from within this code.
577 // However, if there is any code that this Handler calls (such as in
578 // super.handleMessage) that DOES place unexpected messages on the
579 // queue, then we need pass these messages on.
580 Log.d(
581 this,
582 "Unexpected command (CookieWrapper is null): "
583 + msg.what
584 + " ignored by CallerInfoWorkerHandler, passing onto parent.");
585
586 super.handleMessage(msg);
587 } else {
588 Log.d(
589 this,
590 "Processing event: "
591 + cw.event
592 + " token (arg1): "
593 + msg.arg1
594 + " command: "
595 + msg.what
596 + " query URI: "
597 + sanitizeUriToString(args.uri));
598
599 switch (cw.event) {
600 case EVENT_NEW_QUERY:
linyuh183cb712017-12-27 17:02:37 -0800601 final ContentResolver resolver = queryContext.getContentResolver();
Eric Erfanianccca3152017-02-22 16:32:36 -0800602
603 // This should never happen.
604 if (resolver == null) {
605 Log.e(this, "Content Resolver is null!");
606 return;
607 }
linyuh183cb712017-12-27 17:02:37 -0800608 // start the sql command.
Eric Erfanianccca3152017-02-22 16:32:36 -0800609 Cursor cursor;
610 try {
611 cursor =
612 resolver.query(
613 args.uri,
614 args.projection,
615 args.selection,
616 args.selectionArgs,
617 args.orderBy);
618 // Calling getCount() causes the cursor window to be filled,
619 // which will make the first access on the main thread a lot faster.
620 if (cursor != null) {
621 cursor.getCount();
622 }
623 } catch (Exception e) {
624 Log.e(this, "Exception thrown during handling EVENT_ARG_QUERY", e);
625 cursor = null;
626 }
627
628 args.result = cursor;
629 updateData(msg.arg1, cw, cursor);
630 break;
631
632 // shortcuts to avoid query for recognized numbers.
633 case EVENT_EMERGENCY_NUMBER:
634 case EVENT_VOICEMAIL_NUMBER:
635 case EVENT_ADD_LISTENER:
636 updateData(msg.arg1, cw, (Cursor) args.result);
637 break;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700638 default: // fall out
Eric Erfanianccca3152017-02-22 16:32:36 -0800639 }
640 Message reply = args.handler.obtainMessage(msg.what);
641 reply.obj = args;
642 reply.arg1 = msg.arg1;
643
644 reply.sendToTarget();
645 }
646 }
647 }
648 }
649}