• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package android.telecom;
18 
19 import android.app.ActivityManager;
20 import android.compat.annotation.UnsupportedAppUsage;
21 import android.content.AsyncQueryHandler;
22 import android.content.ContentResolver;
23 import android.content.Context;
24 import android.content.pm.PackageManager.NameNotFoundException;
25 import android.database.Cursor;
26 import android.database.SQLException;
27 import android.net.Uri;
28 import android.os.Handler;
29 import android.os.Looper;
30 import android.os.Message;
31 import android.os.SystemClock;
32 import android.os.UserHandle;
33 import android.os.UserManager;
34 import android.provider.ContactsContract.PhoneLookup;
35 import android.telephony.PhoneNumberUtils;
36 import android.telephony.SubscriptionManager;
37 import android.telephony.TelephonyManager;
38 import android.text.TextUtils;
39 
40 import java.util.ArrayList;
41 import java.util.List;
42 
43 /**
44  * Helper class to make it easier to run asynchronous caller-id lookup queries.
45  * @see CallerInfo
46  *
47  * {@hide}
48  */
49 public class CallerInfoAsyncQuery {
50     private static final boolean DBG = false;
51     private static final String LOG_TAG = "CallerInfoAsyncQuery";
52 
53     private static final int EVENT_NEW_QUERY = 1;
54     private static final int EVENT_ADD_LISTENER = 2;
55     private static final int EVENT_END_OF_QUEUE = 3;
56     private static final int EVENT_EMERGENCY_NUMBER = 4;
57     private static final int EVENT_VOICEMAIL_NUMBER = 5;
58     private static final int EVENT_GET_GEO_DESCRIPTION = 6;
59 
60     private CallerInfoAsyncQueryHandler mHandler;
61 
62     // If the CallerInfo query finds no contacts, should we use the
63     // PhoneNumberOfflineGeocoder to look up a "geo description"?
64     // (TODO: This could become a flag in config.xml if it ever needs to be
65     // configured on a per-product basis.)
66     private static final boolean ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION = true;
67 
68     /**
69      * Interface for a CallerInfoAsyncQueryHandler result return.
70      */
71     public interface OnQueryCompleteListener {
72         /**
73          * Called when the query is complete.
74          */
onQueryComplete(int token, Object cookie, CallerInfo ci)75         public void onQueryComplete(int token, Object cookie, CallerInfo ci);
76     }
77 
78 
79     /**
80      * Wrap the cookie from the WorkerArgs with additional information needed by our
81      * classes.
82      */
83     private static final class CookieWrapper {
84         @UnsupportedAppUsage
CookieWrapper()85         private CookieWrapper() {
86         }
87 
88         public OnQueryCompleteListener listener;
89         public Object cookie;
90         public int event;
91         public String number;
92         public String geoDescription;
93 
94         public int subId;
95     }
96 
97 
98     /**
99      * Simple exception used to communicate problems with the query pool.
100      */
101     public static class QueryPoolException extends SQLException {
QueryPoolException(String error)102         public QueryPoolException(String error) {
103             super(error);
104         }
105     }
106 
107     /**
108      * @return {@link ContentResolver} for the "current" user.
109      */
getCurrentProfileContentResolver(Context context)110     static ContentResolver getCurrentProfileContentResolver(Context context) {
111 
112         if (DBG) Log.d(LOG_TAG, "Trying to get current content resolver...");
113 
114         final int currentUser = ActivityManager.getCurrentUser();
115         final int myUser = UserManager.get(context).getProcessUserId();
116 
117         if (DBG) Log.d(LOG_TAG, "myUser=" + myUser + "currentUser=" + currentUser);
118 
119         if (myUser != currentUser) {
120             final Context otherContext;
121             try {
122                 otherContext = context.createPackageContextAsUser(context.getPackageName(),
123                         /* flags =*/ 0, UserHandle.of(currentUser));
124                 return otherContext.getContentResolver();
125             } catch (NameNotFoundException e) {
126                 Log.e(LOG_TAG, e, "Can't find self package");
127                 // Fall back to the primary user.
128             }
129         }
130         return context.getContentResolver();
131     }
132 
133     /**
134      * Our own implementation of the AsyncQueryHandler.
135      */
136     private class CallerInfoAsyncQueryHandler extends AsyncQueryHandler {
137 
138         /*
139          * The information relevant to each CallerInfo query.  Each query may have multiple
140          * listeners, so each AsyncCursorInfo is associated with 2 or more CookieWrapper
141          * objects in the queue (one with a new query event, and one with a end event, with
142          * 0 or more additional listeners in between).
143          */
144 
145         /**
146          * Context passed by the caller.
147          *
148          * NOTE: The actual context we use for query may *not* be this context; since we query
149          * against the "current" contacts provider.  In the constructor we pass the "current"
150          * context resolver (obtained via {@link #getCurrentProfileContentResolver) and pass it
151          * to the super class.
152          */
153         private Context mContext;
154         private Uri mQueryUri;
155         private CallerInfo mCallerInfo;
156         private List<Runnable> mPendingListenerCallbacks = new ArrayList<>();
157 
158         /**
159          * Our own query worker thread.
160          *
161          * This thread handles the messages enqueued in the looper.  The normal sequence
162          * of events is that a new query shows up in the looper queue, followed by 0 or
163          * more add listener requests, and then an end request.  Of course, these requests
164          * can be interlaced with requests from other tokens, but is irrelevant to this
165          * handler since the handler has no state.
166          *
167          * Note that we depend on the queue to keep things in order; in other words, the
168          * looper queue must be FIFO with respect to input from the synchronous startQuery
169          * calls and output to this handleMessage call.
170          *
171          * This use of the queue is required because CallerInfo objects may be accessed
172          * multiple times before the query is complete.  All accesses (listeners) must be
173          * queued up and informed in order when the query is complete.
174          */
175         protected class CallerInfoWorkerHandler extends WorkerHandler {
CallerInfoWorkerHandler(Looper looper)176             public CallerInfoWorkerHandler(Looper looper) {
177                 super(looper);
178             }
179 
180             @Override
handleMessage(Message msg)181             public void handleMessage(Message msg) {
182                 WorkerArgs args = (WorkerArgs) msg.obj;
183                 CookieWrapper cw = (CookieWrapper) args.cookie;
184 
185                 if (cw == null) {
186                     // Normally, this should never be the case for calls originating
187                     // from within this code.
188                     // However, if there is any code that this Handler calls (such as in
189                     // super.handleMessage) that DOES place unexpected messages on the
190                     // queue, then we need pass these messages on.
191                     Log.i(LOG_TAG, "Unexpected command (CookieWrapper is null): " + msg.what +
192                             " ignored by CallerInfoWorkerHandler, passing onto parent.");
193 
194                     super.handleMessage(msg);
195                 } else {
196 
197                     Log.d(LOG_TAG, "Processing event: " + cw.event + " token (arg1): " + msg.arg1 +
198                         " command: " + msg.what + " query URI: " + sanitizeUriToString(args.uri));
199 
200                     switch (cw.event) {
201                         case EVENT_NEW_QUERY:
202                             //start the sql command.
203                             super.handleMessage(msg);
204                             break;
205 
206                         // shortcuts to avoid query for recognized numbers.
207                         case EVENT_EMERGENCY_NUMBER:
208                         case EVENT_VOICEMAIL_NUMBER:
209 
210                         case EVENT_ADD_LISTENER:
211                         case EVENT_END_OF_QUEUE:
212                             // query was already completed, so just send the reply.
213                             // passing the original token value back to the caller
214                             // on top of the event values in arg1.
215                             Message reply = args.handler.obtainMessage(msg.what);
216                             reply.obj = args;
217                             reply.arg1 = msg.arg1;
218 
219                             reply.sendToTarget();
220 
221                             break;
222                         case EVENT_GET_GEO_DESCRIPTION:
223                             handleGeoDescription(msg);
224                             break;
225                         default:
226                     }
227                 }
228             }
229 
handleGeoDescription(Message msg)230             private void handleGeoDescription(Message msg) {
231                 WorkerArgs args = (WorkerArgs) msg.obj;
232                 CookieWrapper cw = (CookieWrapper) args.cookie;
233                 if (!TextUtils.isEmpty(cw.number) && cw.cookie != null && mContext != null) {
234                     final long startTimeMillis = SystemClock.elapsedRealtime();
235                     cw.geoDescription = CallerInfo.getGeoDescription(mContext, cw.number);
236                     final long duration = SystemClock.elapsedRealtime() - startTimeMillis;
237                     if (duration > 500) {
238                         if (DBG) Log.d(LOG_TAG, "[handleGeoDescription]" +
239                                 "Spends long time to retrieve Geo description: " + duration);
240                     }
241                 }
242                 Message reply = args.handler.obtainMessage(msg.what);
243                 reply.obj = args;
244                 reply.arg1 = msg.arg1;
245                 reply.sendToTarget();
246             }
247         }
248 
249 
250         /**
251          * Asynchronous query handler class for the contact / callerinfo object.
252          */
CallerInfoAsyncQueryHandler(Context context)253         private CallerInfoAsyncQueryHandler(Context context) {
254             super(getCurrentProfileContentResolver(context));
255             mContext = context;
256         }
257 
258         @Override
createHandler(Looper looper)259         protected Handler createHandler(Looper looper) {
260             return new CallerInfoWorkerHandler(looper);
261         }
262 
263         /**
264          * Overrides onQueryComplete from AsyncQueryHandler.
265          *
266          * This method takes into account the state of this class; we construct the CallerInfo
267          * object only once for each set of listeners. When the query thread has done its work
268          * and calls this method, we inform the remaining listeners in the queue, until we're
269          * out of listeners.  Once we get the message indicating that we should expect no new
270          * listeners for this CallerInfo object, we release the AsyncCursorInfo back into the
271          * pool.
272          */
273         @Override
onQueryComplete(int token, Object cookie, Cursor cursor)274         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
275             Log.d(LOG_TAG, "##### onQueryComplete() #####   query complete for token: " + token);
276 
277             //get the cookie and notify the listener.
278             CookieWrapper cw = (CookieWrapper) cookie;
279             if (cw == null) {
280                 // Normally, this should never be the case for calls originating
281                 // from within this code.
282                 // However, if there is any code that calls this method, we should
283                 // check the parameters to make sure they're viable.
284                 Log.i(LOG_TAG, "Cookie is null, ignoring onQueryComplete() request.");
285                 if (cursor != null) {
286                     cursor.close();
287                 }
288                 return;
289             }
290 
291             if (cw.event == EVENT_END_OF_QUEUE) {
292                 for (Runnable r : mPendingListenerCallbacks) {
293                     r.run();
294                 }
295                 mPendingListenerCallbacks.clear();
296 
297                 release();
298                 if (cursor != null) {
299                     cursor.close();
300                 }
301                 return;
302             }
303 
304             // If the cw.event == EVENT_GET_GEO_DESCRIPTION, means it would not be the 1st
305             // time entering the onQueryComplete(), mCallerInfo should not be null.
306             if (cw.event == EVENT_GET_GEO_DESCRIPTION) {
307                 if (mCallerInfo != null) {
308                     mCallerInfo.geoDescription = cw.geoDescription;
309                 }
310                 // notify that we can clean up the queue after this.
311                 CookieWrapper endMarker = new CookieWrapper();
312                 endMarker.event = EVENT_END_OF_QUEUE;
313                 startQuery(token, endMarker, null, null, null, null, null);
314             }
315 
316             // check the token and if needed, create the callerinfo object.
317             if (mCallerInfo == null) {
318                 if ((mContext == null) || (mQueryUri == null)) {
319                     throw new QueryPoolException
320                             ("Bad context or query uri, or CallerInfoAsyncQuery already released.");
321                 }
322 
323                 // adjust the callerInfo data as needed, and only if it was set from the
324                 // initial query request.
325                 // Change the callerInfo number ONLY if it is an emergency number or the
326                 // voicemail number, and adjust other data (including photoResource)
327                 // accordingly.
328                 if (cw.event == EVENT_EMERGENCY_NUMBER) {
329                     // Note we're setting the phone number here (refer to javadoc
330                     // comments at the top of CallerInfo class).
331                     mCallerInfo = new CallerInfo().markAsEmergency(mContext);
332                 } else if (cw.event == EVENT_VOICEMAIL_NUMBER) {
333                     mCallerInfo = new CallerInfo().markAsVoiceMail(mContext, cw.subId);
334                 } else {
335                     mCallerInfo = CallerInfo.getCallerInfo(mContext, mQueryUri, cursor);
336                     if (DBG) Log.d(LOG_TAG, "==> Got mCallerInfo: " + mCallerInfo);
337 
338                     CallerInfo newCallerInfo = CallerInfo.doSecondaryLookupIfNecessary(
339                             mContext, cw.number, mCallerInfo);
340                     if (newCallerInfo != mCallerInfo) {
341                         mCallerInfo = newCallerInfo;
342                         if (DBG) Log.d(LOG_TAG, "#####async contact look up with numeric username"
343                                 + mCallerInfo);
344                     }
345 
346                     // Use the number entered by the user for display.
347                     if (!TextUtils.isEmpty(cw.number)) {
348                         mCallerInfo.setPhoneNumber(PhoneNumberUtils.formatNumber(cw.number,
349                                 mCallerInfo.normalizedNumber,
350                                 CallerInfo.getCurrentCountryIso(mContext)));
351                     }
352 
353                     // This condition refer to the google default code for geo.
354                     // If the number exists in Contacts, the CallCard would never show
355                     // the geo description, so it would be unnecessary to query it.
356                     if (ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION) {
357                         if (TextUtils.isEmpty(mCallerInfo.getName())) {
358                             if (DBG) Log.d(LOG_TAG, "start querying geo description");
359                             cw.event = EVENT_GET_GEO_DESCRIPTION;
360                             startQuery(token, cw, null, null, null, null, null);
361                             return;
362                         }
363                     }
364                 }
365 
366                 if (DBG) Log.d(LOG_TAG, "constructing CallerInfo object for token: " + token);
367 
368                 //notify that we can clean up the queue after this.
369                 CookieWrapper endMarker = new CookieWrapper();
370                 endMarker.event = EVENT_END_OF_QUEUE;
371                 startQuery(token, endMarker, null, null, null, null, null);
372             }
373 
374             //notify the listener that the query is complete.
375             if (cw.listener != null) {
376                 mPendingListenerCallbacks.add(new Runnable() {
377                     @Override
378                     public void run() {
379                         if (DBG) Log.d(LOG_TAG, "notifying listener: "
380                                 + cw.listener.getClass().toString() + " for token: " + token
381                                 + mCallerInfo);
382                         cw.listener.onQueryComplete(token, cw.cookie, mCallerInfo);
383                     }
384                 });
385             } else {
386                 Log.w(LOG_TAG, "There is no listener to notify for this query.");
387             }
388 
389             if (cursor != null) {
390                cursor.close();
391             }
392         }
393     }
394 
395     /**
396      * Private constructor for factory methods.
397      */
CallerInfoAsyncQuery()398     private CallerInfoAsyncQuery() {
399     }
400 
401 
402     /**
403      * Factory method to start query with a Uri query spec
404      */
startQuery(int token, Context context, Uri contactRef, OnQueryCompleteListener listener, Object cookie)405     public static CallerInfoAsyncQuery startQuery(int token, Context context, Uri contactRef,
406             OnQueryCompleteListener listener, Object cookie) {
407 
408         CallerInfoAsyncQuery c = new CallerInfoAsyncQuery();
409         c.allocate(context, contactRef);
410 
411         if (DBG) Log.d(LOG_TAG, "starting query for URI: " + contactRef + " handler: " + c.toString());
412 
413         //create cookieWrapper, start query
414         CookieWrapper cw = new CookieWrapper();
415         cw.listener = listener;
416         cw.cookie = cookie;
417         cw.event = EVENT_NEW_QUERY;
418 
419         c.mHandler.startQuery(token, cw, contactRef, null, null, null, null);
420 
421         return c;
422     }
423 
424     /**
425      * Factory method to start the query based on a number.
426      *
427      * Note: if the number contains an "@" character we treat it
428      * as a SIP address, and look it up directly in the Data table
429      * rather than using the PhoneLookup table.
430      * TODO: But eventually we should expose two separate methods, one for
431      * numbers and one for SIP addresses, and then have
432      * PhoneUtils.startGetCallerInfo() decide which one to call based on
433      * the phone type of the incoming connection.
434      */
startQuery(int token, Context context, String number, OnQueryCompleteListener listener, Object cookie)435     public static CallerInfoAsyncQuery startQuery(int token, Context context, String number,
436             OnQueryCompleteListener listener, Object cookie) {
437 
438         int subId = SubscriptionManager.getDefaultSubscriptionId();
439         return startQuery(token, context, number, listener, cookie, subId);
440     }
441 
442     /**
443      * Factory method to start the query based on a number with specific subscription.
444      *
445      * Note: if the number contains an "@" character we treat it
446      * as a SIP address, and look it up directly in the Data table
447      * rather than using the PhoneLookup table.
448      * TODO: But eventually we should expose two separate methods, one for
449      * numbers and one for SIP addresses, and then have
450      * PhoneUtils.startGetCallerInfo() decide which one to call based on
451      * the phone type of the incoming connection.
452      */
startQuery(int token, Context context, String number, OnQueryCompleteListener listener, Object cookie, int subId)453     public static CallerInfoAsyncQuery startQuery(int token, Context context, String number,
454             OnQueryCompleteListener listener, Object cookie, int subId) {
455 
456         if (DBG) {
457             Log.d(LOG_TAG, "##### CallerInfoAsyncQuery startQuery()... #####");
458             Log.d(LOG_TAG, "- number: " + /*number*/ "xxxxxxx");
459             Log.d(LOG_TAG, "- cookie: " + cookie);
460         }
461 
462         // Construct the URI object and query params, and start the query.
463 
464         final Uri contactRef = PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI.buildUpon()
465                 .appendPath(number)
466                 .appendQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS,
467                         String.valueOf(PhoneNumberUtils.isUriNumber(number)))
468                 .build();
469 
470         if (DBG) {
471             Log.d(LOG_TAG, "==> contactRef: " + sanitizeUriToString(contactRef));
472         }
473 
474         CallerInfoAsyncQuery c = new CallerInfoAsyncQuery();
475         c.allocate(context, contactRef);
476 
477         //create cookieWrapper, start query
478         CookieWrapper cw = new CookieWrapper();
479         cw.listener = listener;
480         cw.cookie = cookie;
481         cw.number = number;
482         cw.subId = subId;
483 
484         // check to see if these are recognized numbers, and use shortcuts if we can.
485         TelephonyManager tm = context.getSystemService(TelephonyManager.class);
486         boolean isEmergencyNumber = false;
487         try {
488             isEmergencyNumber = tm.isEmergencyNumber(number);
489         } catch (IllegalStateException ise) {
490             // Ignore the exception that Telephony is not up. Use PhoneNumberUtils API now.
491             // Ideally the PhoneNumberUtils API needs to be removed once the
492             // telphony service not up issue can be fixed (b/187412989)
493             isEmergencyNumber = PhoneNumberUtils.isLocalEmergencyNumber(context, number);
494         }
495         if (isEmergencyNumber) {
496             cw.event = EVENT_EMERGENCY_NUMBER;
497         } else if (PhoneNumberUtils.isVoiceMailNumber(context, subId, number)) {
498             cw.event = EVENT_VOICEMAIL_NUMBER;
499         } else {
500             cw.event = EVENT_NEW_QUERY;
501         }
502 
503         c.mHandler.startQuery(token,
504                               cw,  // cookie
505                               contactRef,  // uri
506                               null,  // projection
507                               null,  // selection
508                               null,  // selectionArgs
509                               null);  // orderBy
510         return c;
511     }
512 
513     /**
514      * Method to add listeners to a currently running query
515      */
addQueryListener(int token, OnQueryCompleteListener listener, Object cookie)516     public void addQueryListener(int token, OnQueryCompleteListener listener, Object cookie) {
517 
518         if (DBG) Log.d(LOG_TAG, "adding listener to query: "
519                 + sanitizeUriToString(mHandler.mQueryUri) + " handler: " + mHandler.toString());
520 
521         //create cookieWrapper, add query request to end of queue.
522         CookieWrapper cw = new CookieWrapper();
523         cw.listener = listener;
524         cw.cookie = cookie;
525         cw.event = EVENT_ADD_LISTENER;
526 
527         mHandler.startQuery(token, cw, null, null, null, null, null);
528     }
529 
530     /**
531      * Method to create a new CallerInfoAsyncQueryHandler object, ensuring correct
532      * state of context and uri.
533      */
allocate(Context context, Uri contactRef)534     private void allocate(Context context, Uri contactRef) {
535         if ((context == null) || (contactRef == null)){
536             throw new QueryPoolException("Bad context or query uri.");
537         }
538         mHandler = new CallerInfoAsyncQueryHandler(context);
539         mHandler.mQueryUri = contactRef;
540     }
541 
542     /**
543      * Releases the relevant data.
544      */
545     @UnsupportedAppUsage
release()546     private void release() {
547         mHandler.mContext = null;
548         mHandler.mQueryUri = null;
549         mHandler.mCallerInfo = null;
550         mHandler = null;
551     }
552 
sanitizeUriToString(Uri uri)553     private static String sanitizeUriToString(Uri uri) {
554         if (uri != null) {
555             String uriString = uri.toString();
556             int indexOfLastSlash = uriString.lastIndexOf('/');
557             if (indexOfLastSlash > 0) {
558                 return uriString.substring(0, indexOfLastSlash) + "/xxxxxxx";
559             } else {
560                 return uriString;
561             }
562         } else {
563             return "";
564         }
565     }
566 }
567