blob: 35011cb6702cb30ba26fe84fc413d3d87cc2af0e [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;
Eric Erfanianccca3152017-02-22 16:32:36 -080020import android.content.AsyncQueryHandler;
21import android.content.ContentResolver;
22import android.content.Context;
23import android.database.Cursor;
24import android.database.SQLException;
25import android.net.Uri;
Eric Erfanianccca3152017-02-22 16:32:36 -080026import android.os.Handler;
27import android.os.Looper;
28import android.os.Message;
wangqi9982f0d2017-10-11 17:46:07 -070029import android.os.Trace;
Eric Erfanianccca3152017-02-22 16:32:36 -080030import android.provider.ContactsContract;
31import android.provider.ContactsContract.Directory;
32import android.support.annotation.MainThread;
33import android.support.annotation.RequiresPermission;
34import android.support.annotation.WorkerThread;
Eric Erfanianccca3152017-02-22 16:32:36 -080035import android.text.TextUtils;
Eric Erfanianccca3152017-02-22 16:32:36 -080036import com.android.dialer.phonenumbercache.CachedNumberLookupService;
37import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
38import com.android.dialer.phonenumbercache.ContactInfoHelper;
39import com.android.dialer.phonenumbercache.PhoneNumberCache;
linyuha8d677f2018-03-07 17:16:08 -080040import com.android.dialer.phonenumberutil.PhoneNumberHelper;
wangqie37d60c2017-09-27 10:13:49 -070041import com.android.dialer.strictmode.StrictModeUtils;
Eric Erfanianccca3152017-02-22 16:32:36 -080042import java.io.IOException;
43import java.io.InputStream;
44import java.util.ArrayList;
45import java.util.Arrays;
46
47/**
48 * Helper class to make it easier to run asynchronous caller-id lookup queries.
49 *
50 * @see CallerInfo
51 */
Eric Erfanianccca3152017-02-22 16:32:36 -080052public class CallerInfoAsyncQuery {
53
54 /** Interface for a CallerInfoAsyncQueryHandler result return. */
Eric Erfaniand5e47f62017-03-15 14:41:07 -070055 interface OnQueryCompleteListener {
Eric Erfanianccca3152017-02-22 16:32:36 -080056
57 /** Called when the query is complete. */
58 @MainThread
59 void onQueryComplete(int token, Object cookie, CallerInfo ci);
60
61 /** Called when data is loaded. Must be called in worker thread. */
62 @WorkerThread
63 void onDataLoaded(int token, Object cookie, CallerInfo ci);
64 }
65
66 private static final boolean DBG = false;
67 private static final String LOG_TAG = "CallerInfoAsyncQuery";
68
69 private static final int EVENT_NEW_QUERY = 1;
70 private static final int EVENT_ADD_LISTENER = 2;
71 private static final int EVENT_EMERGENCY_NUMBER = 3;
72 private static final int EVENT_VOICEMAIL_NUMBER = 4;
73 // If the CallerInfo query finds no contacts, should we use the
74 // PhoneNumberOfflineGeocoder to look up a "geo description"?
75 // (TODO: This could become a flag in config.xml if it ever needs to be
76 // configured on a per-product basis.)
77 private static final boolean ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION = true;
78 /* Directory lookup related code - START */
79 private static final String[] DIRECTORY_PROJECTION = new String[] {Directory._ID};
80
81 /** Private constructor for factory methods. */
82 private CallerInfoAsyncQuery() {}
83
84 @RequiresPermission(Manifest.permission.READ_CONTACTS)
Eric Erfaniand5e47f62017-03-15 14:41:07 -070085 static void startQuery(
Eric Erfanianccca3152017-02-22 16:32:36 -080086 final int token,
87 final Context context,
88 final CallerInfo info,
89 final OnQueryCompleteListener listener,
90 final Object cookie) {
91 Log.d(LOG_TAG, "##### CallerInfoAsyncQuery startContactProviderQuery()... #####");
92 Log.d(LOG_TAG, "- number: " + info.phoneNumber);
93 Log.d(LOG_TAG, "- cookie: " + cookie);
94
95 OnQueryCompleteListener contactsProviderQueryCompleteListener =
96 new OnQueryCompleteListener() {
97 @Override
98 public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -070099 Log.d(LOG_TAG, "contactsProviderQueryCompleteListener onQueryComplete");
Eric Erfanianccca3152017-02-22 16:32:36 -0800100 // If there are no other directory queries, make sure that the listener is
Eric Erfanian938468d2017-10-24 14:05:52 -0700101 // notified of this result. see a bug
Eric Erfanianccca3152017-02-22 16:32:36 -0800102 if ((ci != null && ci.contactExists)
103 || !startOtherDirectoriesQuery(token, context, info, listener, cookie)) {
104 if (listener != null && ci != null) {
105 listener.onQueryComplete(token, cookie, ci);
106 }
107 }
108 }
109
110 @Override
111 public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700112 Log.d(LOG_TAG, "contactsProviderQueryCompleteListener onDataLoaded");
Eric Erfanianccca3152017-02-22 16:32:36 -0800113 listener.onDataLoaded(token, cookie, ci);
114 }
115 };
116 startDefaultDirectoryQuery(token, context, info, contactsProviderQueryCompleteListener, cookie);
117 }
118
119 // Private methods
120 private static void startDefaultDirectoryQuery(
121 int token,
122 Context context,
123 CallerInfo info,
124 OnQueryCompleteListener listener,
125 Object cookie) {
126 // Construct the URI object and query params, and start the query.
127 Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber);
128 startQueryInternal(token, context, info, listener, cookie, uri);
129 }
130
131 /**
132 * Factory method to start the query based on a CallerInfo object.
133 *
134 * <p>Note: if the number contains an "@" character we treat it as a SIP address, and look it up
135 * directly in the Data table rather than using the PhoneLookup table. TODO: But eventually we
136 * should expose two separate methods, one for numbers and one for SIP addresses, and then have
137 * PhoneUtils.startGetCallerInfo() decide which one to call based on the phone type of the
138 * incoming connection.
139 */
140 private static void startQueryInternal(
141 int token,
142 Context context,
143 CallerInfo info,
144 OnQueryCompleteListener listener,
145 Object cookie,
146 Uri contactRef) {
147 if (DBG) {
148 Log.d(LOG_TAG, "==> contactRef: " + sanitizeUriToString(contactRef));
149 }
150
151 if ((context == null) || (contactRef == null)) {
152 throw new QueryPoolException("Bad context or query uri.");
153 }
154 CallerInfoAsyncQueryHandler handler = new CallerInfoAsyncQueryHandler(context, contactRef);
155
156 //create cookieWrapper, start query
157 CookieWrapper cw = new CookieWrapper();
158 cw.listener = listener;
159 cw.cookie = cookie;
160 cw.number = info.phoneNumber;
wangqi1420a222017-09-21 09:37:40 -0700161 cw.countryIso = info.countryIso;
Eric Erfanianccca3152017-02-22 16:32:36 -0800162
163 // check to see if these are recognized numbers, and use shortcuts if we can.
linyuha8d677f2018-03-07 17:16:08 -0800164 if (PhoneNumberHelper.isLocalEmergencyNumber(context, info.phoneNumber)) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800165 cw.event = EVENT_EMERGENCY_NUMBER;
166 } else if (info.isVoiceMailNumber()) {
167 cw.event = EVENT_VOICEMAIL_NUMBER;
168 } else {
169 cw.event = EVENT_NEW_QUERY;
170 }
171
linyuh437ae952018-03-26 12:46:18 -0700172 String[] proejection = CallerInfo.getDefaultPhoneLookupProjection();
Eric Erfanianccca3152017-02-22 16:32:36 -0800173 handler.startQuery(
174 token,
175 cw, // cookie
176 contactRef, // uri
177 proejection, // projection
178 null, // selection
179 null, // selectionArgs
180 null); // orderBy
181 }
182
183 // Return value indicates if listener was notified.
184 private static boolean startOtherDirectoriesQuery(
185 int token,
186 Context context,
187 CallerInfo info,
188 OnQueryCompleteListener listener,
189 Object cookie) {
wangqi9982f0d2017-10-11 17:46:07 -0700190 Trace.beginSection("CallerInfoAsyncQuery.startOtherDirectoriesQuery");
wangqie37d60c2017-09-27 10:13:49 -0700191 long[] directoryIds = StrictModeUtils.bypass(() -> getDirectoryIds(context));
Eric Erfanianccca3152017-02-22 16:32:36 -0800192 int size = directoryIds.length;
193 if (size == 0) {
wangqi9982f0d2017-10-11 17:46:07 -0700194 Trace.endSection();
Eric Erfanianccca3152017-02-22 16:32:36 -0800195 return false;
196 }
197
198 DirectoryQueryCompleteListenerFactory listenerFactory =
199 new DirectoryQueryCompleteListenerFactory(context, size, listener);
200
201 // The current implementation of multiple async query runs in single handler thread
202 // in AsyncQueryHandler.
203 // intermediateListener.onQueryComplete is also called from the same caller thread.
Eric Erfanian938468d2017-10-24 14:05:52 -0700204 // TODO(a bug): use thread pool instead of single thread.
Eric Erfanianccca3152017-02-22 16:32:36 -0800205 for (int i = 0; i < size; i++) {
206 long directoryId = directoryIds[i];
207 Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber, directoryId);
208 if (DBG) {
209 Log.d(LOG_TAG, "directoryId: " + directoryId + " uri: " + uri);
210 }
211 OnQueryCompleteListener intermediateListener = listenerFactory.newListener(directoryId);
212 startQueryInternal(token, context, info, intermediateListener, cookie, uri);
213 }
wangqi9982f0d2017-10-11 17:46:07 -0700214 Trace.endSection();
Eric Erfanianccca3152017-02-22 16:32:36 -0800215 return true;
216 }
217
218 private static long[] getDirectoryIds(Context context) {
219 ArrayList<Long> results = new ArrayList<>();
220
linyuh437ae952018-03-26 12:46:18 -0700221 Uri uri = Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "directories_enterprise");
Eric Erfanianccca3152017-02-22 16:32:36 -0800222
223 ContentResolver cr = context.getContentResolver();
224 Cursor cursor = cr.query(uri, DIRECTORY_PROJECTION, null, null, null);
225 addDirectoryIdsFromCursor(cursor, results);
226
227 long[] result = new long[results.size()];
228 for (int i = 0; i < results.size(); i++) {
229 result[i] = results.get(i);
230 }
231 return result;
232 }
233
234 private static void addDirectoryIdsFromCursor(Cursor cursor, ArrayList<Long> results) {
235 if (cursor != null) {
236 int idIndex = cursor.getColumnIndex(Directory._ID);
237 while (cursor.moveToNext()) {
238 long id = cursor.getLong(idIndex);
linyuh437ae952018-03-26 12:46:18 -0700239 if (Directory.isRemoteDirectoryId(id)) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800240 results.add(id);
241 }
242 }
243 cursor.close();
244 }
245 }
246
247 private static String sanitizeUriToString(Uri uri) {
248 if (uri != null) {
249 String uriString = uri.toString();
250 int indexOfLastSlash = uriString.lastIndexOf('/');
251 if (indexOfLastSlash > 0) {
252 return uriString.substring(0, indexOfLastSlash) + "/xxxxxxx";
253 } else {
254 return uriString;
255 }
256 } else {
257 return "";
258 }
259 }
260
261 /** Wrap the cookie from the WorkerArgs with additional information needed by our classes. */
262 private static final class CookieWrapper {
263
264 public OnQueryCompleteListener listener;
265 public Object cookie;
266 public int event;
267 public String number;
wangqi1420a222017-09-21 09:37:40 -0700268 public String countryIso;
Eric Erfanianccca3152017-02-22 16:32:36 -0800269 }
270 /* Directory lookup related code - END */
271
272 /** Simple exception used to communicate problems with the query pool. */
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700273 private static class QueryPoolException extends SQLException {
Eric Erfanianccca3152017-02-22 16:32:36 -0800274
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700275 QueryPoolException(String error) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800276 super(error);
277 }
278 }
279
280 private static final class DirectoryQueryCompleteListenerFactory {
281
linyuh183cb712017-12-27 17:02:37 -0800282 private final OnQueryCompleteListener listener;
283 private final Context context;
Eric Erfanianccca3152017-02-22 16:32:36 -0800284 // Make sure listener to be called once and only once
linyuh183cb712017-12-27 17:02:37 -0800285 private int count;
286 private boolean isListenerCalled;
Eric Erfanianccca3152017-02-22 16:32:36 -0800287
288 DirectoryQueryCompleteListenerFactory(
289 Context context, int size, OnQueryCompleteListener listener) {
linyuh183cb712017-12-27 17:02:37 -0800290 count = size;
291 this.listener = listener;
292 isListenerCalled = false;
293 this.context = context;
Eric Erfanianccca3152017-02-22 16:32:36 -0800294 }
295
296 private void onDirectoryQueryComplete(
297 int token, Object cookie, CallerInfo ci, long directoryId) {
298 boolean shouldCallListener = false;
299 synchronized (this) {
linyuh183cb712017-12-27 17:02:37 -0800300 count = count - 1;
301 if (!isListenerCalled && (ci.contactExists || count == 0)) {
302 isListenerCalled = true;
Eric Erfanianccca3152017-02-22 16:32:36 -0800303 shouldCallListener = true;
304 }
305 }
306
307 // Don't call callback in synchronized block because mListener.onQueryComplete may
308 // take long time to complete
linyuh183cb712017-12-27 17:02:37 -0800309 if (shouldCallListener && listener != null) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800310 addCallerInfoIntoCache(ci, directoryId);
linyuh183cb712017-12-27 17:02:37 -0800311 listener.onQueryComplete(token, cookie, ci);
Eric Erfanianccca3152017-02-22 16:32:36 -0800312 }
313 }
314
315 private void addCallerInfoIntoCache(CallerInfo ci, long directoryId) {
316 CachedNumberLookupService cachedNumberLookupService =
linyuh183cb712017-12-27 17:02:37 -0800317 PhoneNumberCache.get(context).getCachedNumberLookupService();
Eric Erfanianccca3152017-02-22 16:32:36 -0800318 if (ci.contactExists && cachedNumberLookupService != null) {
319 // 1. Cache caller info
320 CachedContactInfo cachedContactInfo =
321 CallerInfoUtils.buildCachedContactInfo(cachedNumberLookupService, ci);
linyuh183cb712017-12-27 17:02:37 -0800322 String directoryLabel = context.getString(R.string.directory_search_label);
Eric Erfanianccca3152017-02-22 16:32:36 -0800323 cachedContactInfo.setDirectorySource(directoryLabel, directoryId);
linyuh183cb712017-12-27 17:02:37 -0800324 cachedNumberLookupService.addContact(context, cachedContactInfo);
Eric Erfanianccca3152017-02-22 16:32:36 -0800325
326 // 2. Cache photo
327 if (ci.contactDisplayPhotoUri != null && ci.normalizedNumber != null) {
328 try (InputStream in =
linyuh183cb712017-12-27 17:02:37 -0800329 context.getContentResolver().openInputStream(ci.contactDisplayPhotoUri)) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800330 if (in != null) {
linyuh183cb712017-12-27 17:02:37 -0800331 cachedNumberLookupService.addPhoto(context, ci.normalizedNumber, in);
Eric Erfanianccca3152017-02-22 16:32:36 -0800332 }
333 } catch (IOException e) {
334 Log.e(LOG_TAG, "failed to fetch directory contact photo", e);
335 }
336 }
337 }
338 }
339
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700340 OnQueryCompleteListener newListener(long directoryId) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800341 return new DirectoryQueryCompleteListener(directoryId);
342 }
343
344 private class DirectoryQueryCompleteListener implements OnQueryCompleteListener {
345
linyuh183cb712017-12-27 17:02:37 -0800346 private final long directoryId;
Eric Erfanianccca3152017-02-22 16:32:36 -0800347
348 DirectoryQueryCompleteListener(long directoryId) {
linyuh183cb712017-12-27 17:02:37 -0800349 this.directoryId = directoryId;
Eric Erfanianccca3152017-02-22 16:32:36 -0800350 }
351
352 @Override
353 public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700354 Log.d(LOG_TAG, "DirectoryQueryCompleteListener.onDataLoaded");
linyuh183cb712017-12-27 17:02:37 -0800355 listener.onDataLoaded(token, cookie, ci);
Eric Erfanianccca3152017-02-22 16:32:36 -0800356 }
357
358 @Override
359 public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700360 Log.d(LOG_TAG, "DirectoryQueryCompleteListener.onQueryComplete");
linyuh183cb712017-12-27 17:02:37 -0800361 onDirectoryQueryComplete(token, cookie, ci, directoryId);
Eric Erfanianccca3152017-02-22 16:32:36 -0800362 }
363 }
364 }
365
366 /** Our own implementation of the AsyncQueryHandler. */
367 private static class CallerInfoAsyncQueryHandler extends AsyncQueryHandler {
368
369 /**
370 * The information relevant to each CallerInfo query. Each query may have multiple listeners, so
371 * each AsyncCursorInfo is associated with 2 or more CookieWrapper objects in the queue (one
372 * with a new query event, and one with a end event, with 0 or more additional listeners in
373 * between).
374 */
linyuh183cb712017-12-27 17:02:37 -0800375 private Context queryContext;
Eric Erfanianccca3152017-02-22 16:32:36 -0800376
linyuh183cb712017-12-27 17:02:37 -0800377 private Uri queryUri;
378 private CallerInfo callerInfo;
Eric Erfanianccca3152017-02-22 16:32:36 -0800379
380 /** Asynchronous query handler class for the contact / callerinfo object. */
381 private CallerInfoAsyncQueryHandler(Context context, Uri contactRef) {
382 super(context.getContentResolver());
linyuh183cb712017-12-27 17:02:37 -0800383 this.queryContext = context;
384 this.queryUri = contactRef;
Eric Erfanianccca3152017-02-22 16:32:36 -0800385 }
386
387 @Override
388 public void startQuery(
389 int token,
390 Object cookie,
391 Uri uri,
392 String[] projection,
393 String selection,
394 String[] selectionArgs,
395 String orderBy) {
396 if (DBG) {
397 // Show stack trace with the arguments.
398 Log.d(
399 LOG_TAG,
400 "InCall: startQuery: url="
401 + uri
402 + " projection=["
403 + Arrays.toString(projection)
404 + "]"
405 + " selection="
406 + selection
407 + " "
408 + " args=["
409 + Arrays.toString(selectionArgs)
410 + "]",
411 new RuntimeException("STACKTRACE"));
412 }
413 super.startQuery(token, cookie, uri, projection, selection, selectionArgs, orderBy);
414 }
415
416 @Override
417 protected Handler createHandler(Looper looper) {
418 return new CallerInfoWorkerHandler(looper);
419 }
420
421 /**
422 * Overrides onQueryComplete from AsyncQueryHandler.
423 *
424 * <p>This method takes into account the state of this class; we construct the CallerInfo object
425 * only once for each set of listeners. When the query thread has done its work and calls this
426 * method, we inform the remaining listeners in the queue, until we're out of listeners. Once we
427 * get the message indicating that we should expect no new listeners for this CallerInfo object,
428 * we release the AsyncCursorInfo back into the pool.
429 */
430 @Override
431 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
432 Log.d(this, "##### onQueryComplete() ##### query complete for token: " + token);
433
434 CookieWrapper cw = (CookieWrapper) cookie;
435
436 if (cw.listener != null) {
437 Log.d(
438 this,
439 "notifying listener: "
440 + cw.listener.getClass().toString()
441 + " for token: "
442 + token
linyuh183cb712017-12-27 17:02:37 -0800443 + callerInfo);
444 cw.listener.onQueryComplete(token, cw.cookie, callerInfo);
Eric Erfanianccca3152017-02-22 16:32:36 -0800445 }
linyuh183cb712017-12-27 17:02:37 -0800446 queryContext = null;
447 queryUri = null;
448 callerInfo = null;
Eric Erfanianccca3152017-02-22 16:32:36 -0800449 }
450
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700451 void updateData(int token, Object cookie, Cursor cursor) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800452 try {
453 Log.d(this, "##### updateData() ##### for token: " + token);
454
455 //get the cookie and notify the listener.
456 CookieWrapper cw = (CookieWrapper) cookie;
457 if (cw == null) {
458 // Normally, this should never be the case for calls originating
459 // from within this code.
460 // However, if there is any code that calls this method, we should
461 // check the parameters to make sure they're viable.
462 Log.d(this, "Cookie is null, ignoring onQueryComplete() request.");
463 return;
464 }
465
466 // check the token and if needed, create the callerinfo object.
linyuh183cb712017-12-27 17:02:37 -0800467 if (callerInfo == null) {
468 if ((queryContext == null) || (queryUri == null)) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800469 throw new QueryPoolException(
470 "Bad context or query uri, or CallerInfoAsyncQuery already released.");
471 }
472
473 // adjust the callerInfo data as needed, and only if it was set from the
474 // initial query request.
475 // Change the callerInfo number ONLY if it is an emergency number or the
476 // voicemail number, and adjust other data (including photoResource)
477 // accordingly.
478 if (cw.event == EVENT_EMERGENCY_NUMBER) {
479 // Note we're setting the phone number here (refer to javadoc
480 // comments at the top of CallerInfo class).
linyuh183cb712017-12-27 17:02:37 -0800481 callerInfo = new CallerInfo().markAsEmergency(queryContext);
Eric Erfanianccca3152017-02-22 16:32:36 -0800482 } else if (cw.event == EVENT_VOICEMAIL_NUMBER) {
linyuh183cb712017-12-27 17:02:37 -0800483 callerInfo = new CallerInfo().markAsVoiceMail(queryContext);
Eric Erfanianccca3152017-02-22 16:32:36 -0800484 } else {
linyuh183cb712017-12-27 17:02:37 -0800485 callerInfo = CallerInfo.getCallerInfo(queryContext, queryUri, cursor);
486 Log.d(this, "==> Got mCallerInfo: " + callerInfo);
Eric Erfanianccca3152017-02-22 16:32:36 -0800487
488 CallerInfo newCallerInfo =
linyuh183cb712017-12-27 17:02:37 -0800489 CallerInfo.doSecondaryLookupIfNecessary(queryContext, cw.number, callerInfo);
490 if (newCallerInfo != callerInfo) {
491 callerInfo = newCallerInfo;
492 Log.d(this, "#####async contact look up with numeric username" + callerInfo);
Eric Erfanianccca3152017-02-22 16:32:36 -0800493 }
linyuh183cb712017-12-27 17:02:37 -0800494 callerInfo.countryIso = cw.countryIso;
Eric Erfanianccca3152017-02-22 16:32:36 -0800495
496 // Final step: look up the geocoded description.
497 if (ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION) {
498 // Note we do this only if we *don't* have a valid name (i.e. if
499 // no contacts matched the phone number of the incoming call),
500 // since that's the only case where the incoming-call UI cares
501 // about this field.
502 //
503 // (TODO: But if we ever want the UI to show the geoDescription
504 // even when we *do* match a contact, we'll need to either call
505 // updateGeoDescription() unconditionally here, or possibly add a
506 // new parameter to CallerInfoAsyncQuery.startQuery() to force
507 // the geoDescription field to be populated.)
508
linyuh183cb712017-12-27 17:02:37 -0800509 if (TextUtils.isEmpty(callerInfo.name)) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800510 // Actually when no contacts match the incoming phone number,
511 // the CallerInfo object is totally blank here (i.e. no name
512 // *or* phoneNumber). So we need to pass in cw.number as
513 // a fallback number.
linyuh183cb712017-12-27 17:02:37 -0800514 callerInfo.updateGeoDescription(queryContext, cw.number);
Eric Erfanianccca3152017-02-22 16:32:36 -0800515 }
516 }
517
518 // Use the number entered by the user for display.
519 if (!TextUtils.isEmpty(cw.number)) {
linyuh183cb712017-12-27 17:02:37 -0800520 callerInfo.phoneNumber = cw.number;
Eric Erfanianccca3152017-02-22 16:32:36 -0800521 }
522 }
523
524 Log.d(this, "constructing CallerInfo object for token: " + token);
525
526 if (cw.listener != null) {
linyuh183cb712017-12-27 17:02:37 -0800527 cw.listener.onDataLoaded(token, cw.cookie, callerInfo);
Eric Erfanianccca3152017-02-22 16:32:36 -0800528 }
529 }
530
531 } finally {
532 // The cursor may have been closed in CallerInfo.getCallerInfo()
533 if (cursor != null && !cursor.isClosed()) {
534 cursor.close();
535 }
536 }
537 }
538
539 /**
540 * Our own query worker thread.
541 *
542 * <p>This thread handles the messages enqueued in the looper. The normal sequence of events is
543 * that a new query shows up in the looper queue, followed by 0 or more add listener requests,
544 * and then an end request. Of course, these requests can be interlaced with requests from other
545 * tokens, but is irrelevant to this handler since the handler has no state.
546 *
547 * <p>Note that we depend on the queue to keep things in order; in other words, the looper queue
548 * must be FIFO with respect to input from the synchronous startQuery calls and output to this
549 * handleMessage call.
550 *
551 * <p>This use of the queue is required because CallerInfo objects may be accessed multiple
552 * times before the query is complete. All accesses (listeners) must be queued up and informed
553 * in order when the query is complete.
554 */
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700555 class CallerInfoWorkerHandler extends WorkerHandler {
Eric Erfanianccca3152017-02-22 16:32:36 -0800556
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700557 CallerInfoWorkerHandler(Looper looper) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800558 super(looper);
559 }
560
561 @Override
562 public void handleMessage(Message msg) {
563 WorkerArgs args = (WorkerArgs) msg.obj;
564 CookieWrapper cw = (CookieWrapper) args.cookie;
565
566 if (cw == null) {
567 // Normally, this should never be the case for calls originating
568 // from within this code.
569 // However, if there is any code that this Handler calls (such as in
570 // super.handleMessage) that DOES place unexpected messages on the
571 // queue, then we need pass these messages on.
572 Log.d(
573 this,
574 "Unexpected command (CookieWrapper is null): "
575 + msg.what
576 + " ignored by CallerInfoWorkerHandler, passing onto parent.");
577
578 super.handleMessage(msg);
579 } else {
580 Log.d(
581 this,
582 "Processing event: "
583 + cw.event
584 + " token (arg1): "
585 + msg.arg1
586 + " command: "
587 + msg.what
588 + " query URI: "
589 + sanitizeUriToString(args.uri));
590
591 switch (cw.event) {
592 case EVENT_NEW_QUERY:
linyuh183cb712017-12-27 17:02:37 -0800593 final ContentResolver resolver = queryContext.getContentResolver();
Eric Erfanianccca3152017-02-22 16:32:36 -0800594
595 // This should never happen.
596 if (resolver == null) {
597 Log.e(this, "Content Resolver is null!");
598 return;
599 }
linyuh183cb712017-12-27 17:02:37 -0800600 // start the sql command.
Eric Erfanianccca3152017-02-22 16:32:36 -0800601 Cursor cursor;
602 try {
603 cursor =
604 resolver.query(
605 args.uri,
606 args.projection,
607 args.selection,
608 args.selectionArgs,
609 args.orderBy);
610 // Calling getCount() causes the cursor window to be filled,
611 // which will make the first access on the main thread a lot faster.
612 if (cursor != null) {
613 cursor.getCount();
614 }
615 } catch (Exception e) {
616 Log.e(this, "Exception thrown during handling EVENT_ARG_QUERY", e);
617 cursor = null;
618 }
619
620 args.result = cursor;
621 updateData(msg.arg1, cw, cursor);
622 break;
623
624 // shortcuts to avoid query for recognized numbers.
625 case EVENT_EMERGENCY_NUMBER:
626 case EVENT_VOICEMAIL_NUMBER:
627 case EVENT_ADD_LISTENER:
628 updateData(msg.arg1, cw, (Cursor) args.result);
629 break;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700630 default: // fall out
Eric Erfanianccca3152017-02-22 16:32:36 -0800631 }
632 Message reply = args.handler.obtainMessage(msg.what);
633 reply.obj = args;
634 reply.arg1 = msg.arg1;
635
636 reply.sendToTarget();
637 }
638 }
639 }
640 }
641}