Update dialer sources.
Test: Built package and system image.
This change clobbers the old source, and is an export
from an internal Google repository.
The internal repository was forked form Android in March,
and this change includes modifications since then, to
near the v8 release.
Since the fork, we've moved code from monolithic to independent modules. In addition,
we've switched to Blaze/Bazel as the build sysetm. This export, however, still uses make.
New dependencies have been added:
- Dagger
- Auto-Value
- Glide
- Libshortcutbadger
Going forward, development will still be in Google3, and the Gerrit release
will become an automated export, with the next drop happening in ~ two weeks.
Android.mk includes local modifications from ToT.
Abridged changelog:
Bug fixes
● Not able to mute, add a call when using Phone app in multiwindow mode
● Double tap on keypad triggering multiple key and tones
● Reported spam numbers not showing as spam in the call log
● Crash when user tries to block number while Phone app is not set as default
● Crash when user picks a number from search auto-complete list
Visual Voicemail (VVM) improvements
● Share Voicemail audio via standard exporting mechanisms that support file attachment
(email, MMS, etc.)
● Make phone number, email and web sites in VVM transcript clickable
● Set PIN before declining VVM Terms of Service {Carrier}
● Set client type for outbound visual voicemail SMS {Carrier}
New incoming call and incall UI on older devices
(Android M)
● Updated Phone app icon
● New incall UI (large buttons, button labels)
● New and animated Answer/Reject gestures
Accessibility
● Add custom answer/decline call buttons on answer screen for touch exploration
accessibility services
● Increase size of touch target
● Add verbal feedback when a Voicemail fails to load
● Fix pressing of Phone buttons while in a phone call using Switch Access
● Fix selecting and opening contacts in talkback mode
● Split focus for ‘Learn More’ link in caller id & spam to help distinguish similar text
Other
● Backup & Restore for App Preferences
● Prompt user to enable Wi-Fi calling if the call ends due to out of service and Wi-Fi is
connected
● Rename “Dialpad” to “Keypad”
● Show "Private number" for restricted calls
● Delete unused items (vcard, add contact, call history) from Phone menu
Change-Id: I2a7e53532a24c21bf308bf0a6d178d7ddbca4958
diff --git a/java/com/android/incallui/CallerInfoAsyncQuery.java b/java/com/android/incallui/CallerInfoAsyncQuery.java
new file mode 100644
index 0000000..f8d7ac6
--- /dev/null
+++ b/java/com/android/incallui/CallerInfoAsyncQuery.java
@@ -0,0 +1,638 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Directory;
+import android.support.annotation.MainThread;
+import android.support.annotation.RequiresPermission;
+import android.support.annotation.WorkerThread;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import com.android.contacts.common.compat.DirectoryCompat;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+import com.android.dialer.phonenumbercache.PhoneNumberCache;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Helper class to make it easier to run asynchronous caller-id lookup queries.
+ *
+ * @see CallerInfo
+ */
+@TargetApi(VERSION_CODES.M)
+public class CallerInfoAsyncQuery {
+
+ /** Interface for a CallerInfoAsyncQueryHandler result return. */
+ public interface OnQueryCompleteListener {
+
+ /** Called when the query is complete. */
+ @MainThread
+ void onQueryComplete(int token, Object cookie, CallerInfo ci);
+
+ /** Called when data is loaded. Must be called in worker thread. */
+ @WorkerThread
+ void onDataLoaded(int token, Object cookie, CallerInfo ci);
+ }
+
+ private static final boolean DBG = false;
+ private static final String LOG_TAG = "CallerInfoAsyncQuery";
+
+ private static final int EVENT_NEW_QUERY = 1;
+ private static final int EVENT_ADD_LISTENER = 2;
+ private static final int EVENT_EMERGENCY_NUMBER = 3;
+ private static final int EVENT_VOICEMAIL_NUMBER = 4;
+ // If the CallerInfo query finds no contacts, should we use the
+ // PhoneNumberOfflineGeocoder to look up a "geo description"?
+ // (TODO: This could become a flag in config.xml if it ever needs to be
+ // configured on a per-product basis.)
+ private static final boolean ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION = true;
+ /* Directory lookup related code - START */
+ private static final String[] DIRECTORY_PROJECTION = new String[] {Directory._ID};
+
+ /** Private constructor for factory methods. */
+ private CallerInfoAsyncQuery() {}
+
+ @RequiresPermission(Manifest.permission.READ_CONTACTS)
+ public static void startQuery(
+ final int token,
+ final Context context,
+ final CallerInfo info,
+ final OnQueryCompleteListener listener,
+ final Object cookie) {
+ Log.d(LOG_TAG, "##### CallerInfoAsyncQuery startContactProviderQuery()... #####");
+ Log.d(LOG_TAG, "- number: " + info.phoneNumber);
+ Log.d(LOG_TAG, "- cookie: " + cookie);
+
+ OnQueryCompleteListener contactsProviderQueryCompleteListener =
+ new OnQueryCompleteListener() {
+ @Override
+ public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
+ Log.d(LOG_TAG, "contactsProviderQueryCompleteListener done");
+ // If there are no other directory queries, make sure that the listener is
+ // notified of this result. see b/27621628
+ if ((ci != null && ci.contactExists)
+ || !startOtherDirectoriesQuery(token, context, info, listener, cookie)) {
+ if (listener != null && ci != null) {
+ listener.onQueryComplete(token, cookie, ci);
+ }
+ }
+ }
+
+ @Override
+ public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
+ listener.onDataLoaded(token, cookie, ci);
+ }
+ };
+ startDefaultDirectoryQuery(token, context, info, contactsProviderQueryCompleteListener, cookie);
+ }
+
+ // Private methods
+ private static void startDefaultDirectoryQuery(
+ int token,
+ Context context,
+ CallerInfo info,
+ OnQueryCompleteListener listener,
+ Object cookie) {
+ // Construct the URI object and query params, and start the query.
+ Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber);
+ startQueryInternal(token, context, info, listener, cookie, uri);
+ }
+
+ /**
+ * Factory method to start the query based on a CallerInfo object.
+ *
+ * <p>Note: if the number contains an "@" character we treat it as a SIP address, and look it up
+ * directly in the Data table rather than using the PhoneLookup table. TODO: But eventually we
+ * should expose two separate methods, one for numbers and one for SIP addresses, and then have
+ * PhoneUtils.startGetCallerInfo() decide which one to call based on the phone type of the
+ * incoming connection.
+ */
+ private static void startQueryInternal(
+ int token,
+ Context context,
+ CallerInfo info,
+ OnQueryCompleteListener listener,
+ Object cookie,
+ Uri contactRef) {
+ if (DBG) {
+ Log.d(LOG_TAG, "==> contactRef: " + sanitizeUriToString(contactRef));
+ }
+
+ if ((context == null) || (contactRef == null)) {
+ throw new QueryPoolException("Bad context or query uri.");
+ }
+ CallerInfoAsyncQueryHandler handler = new CallerInfoAsyncQueryHandler(context, contactRef);
+
+ //create cookieWrapper, start query
+ CookieWrapper cw = new CookieWrapper();
+ cw.listener = listener;
+ cw.cookie = cookie;
+ cw.number = info.phoneNumber;
+
+ // check to see if these are recognized numbers, and use shortcuts if we can.
+ if (PhoneNumberUtils.isLocalEmergencyNumber(context, info.phoneNumber)) {
+ cw.event = EVENT_EMERGENCY_NUMBER;
+ } else if (info.isVoiceMailNumber()) {
+ cw.event = EVENT_VOICEMAIL_NUMBER;
+ } else {
+ cw.event = EVENT_NEW_QUERY;
+ }
+
+ String[] proejection = CallerInfo.getDefaultPhoneLookupProjection(contactRef);
+ handler.startQuery(
+ token,
+ cw, // cookie
+ contactRef, // uri
+ proejection, // projection
+ null, // selection
+ null, // selectionArgs
+ null); // orderBy
+ }
+
+ // Return value indicates if listener was notified.
+ private static boolean startOtherDirectoriesQuery(
+ int token,
+ Context context,
+ CallerInfo info,
+ OnQueryCompleteListener listener,
+ Object cookie) {
+ long[] directoryIds = getDirectoryIds(context);
+ int size = directoryIds.length;
+ if (size == 0) {
+ return false;
+ }
+
+ DirectoryQueryCompleteListenerFactory listenerFactory =
+ new DirectoryQueryCompleteListenerFactory(context, size, listener);
+
+ // The current implementation of multiple async query runs in single handler thread
+ // in AsyncQueryHandler.
+ // intermediateListener.onQueryComplete is also called from the same caller thread.
+ // TODO(b/26019872): use thread pool instead of single thread.
+ for (int i = 0; i < size; i++) {
+ long directoryId = directoryIds[i];
+ Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber, directoryId);
+ if (DBG) {
+ Log.d(LOG_TAG, "directoryId: " + directoryId + " uri: " + uri);
+ }
+ OnQueryCompleteListener intermediateListener = listenerFactory.newListener(directoryId);
+ startQueryInternal(token, context, info, intermediateListener, cookie, uri);
+ }
+ return true;
+ }
+
+ private static long[] getDirectoryIds(Context context) {
+ ArrayList<Long> results = new ArrayList<>();
+
+ Uri uri = Directory.CONTENT_URI;
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ uri = Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "directories_enterprise");
+ }
+
+ ContentResolver cr = context.getContentResolver();
+ Cursor cursor = cr.query(uri, DIRECTORY_PROJECTION, null, null, null);
+ addDirectoryIdsFromCursor(cursor, results);
+
+ long[] result = new long[results.size()];
+ for (int i = 0; i < results.size(); i++) {
+ result[i] = results.get(i);
+ }
+ return result;
+ }
+
+ private static void addDirectoryIdsFromCursor(Cursor cursor, ArrayList<Long> results) {
+ if (cursor != null) {
+ int idIndex = cursor.getColumnIndex(Directory._ID);
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(idIndex);
+ if (DirectoryCompat.isRemoteDirectoryId(id)) {
+ results.add(id);
+ }
+ }
+ cursor.close();
+ }
+ }
+
+ private static String sanitizeUriToString(Uri uri) {
+ if (uri != null) {
+ String uriString = uri.toString();
+ int indexOfLastSlash = uriString.lastIndexOf('/');
+ if (indexOfLastSlash > 0) {
+ return uriString.substring(0, indexOfLastSlash) + "/xxxxxxx";
+ } else {
+ return uriString;
+ }
+ } else {
+ return "";
+ }
+ }
+
+ /** Wrap the cookie from the WorkerArgs with additional information needed by our classes. */
+ private static final class CookieWrapper {
+
+ public OnQueryCompleteListener listener;
+ public Object cookie;
+ public int event;
+ public String number;
+ }
+ /* Directory lookup related code - END */
+
+ /** Simple exception used to communicate problems with the query pool. */
+ public static class QueryPoolException extends SQLException {
+
+ public QueryPoolException(String error) {
+ super(error);
+ }
+ }
+
+ private static final class DirectoryQueryCompleteListenerFactory {
+
+ private final OnQueryCompleteListener mListener;
+ private final Context mContext;
+ // Make sure listener to be called once and only once
+ private int mCount;
+ private boolean mIsListenerCalled;
+
+ DirectoryQueryCompleteListenerFactory(
+ Context context, int size, OnQueryCompleteListener listener) {
+ mCount = size;
+ mListener = listener;
+ mIsListenerCalled = false;
+ mContext = context;
+ }
+
+ private void onDirectoryQueryComplete(
+ int token, Object cookie, CallerInfo ci, long directoryId) {
+ boolean shouldCallListener = false;
+ synchronized (this) {
+ mCount = mCount - 1;
+ if (!mIsListenerCalled && (ci.contactExists || mCount == 0)) {
+ mIsListenerCalled = true;
+ shouldCallListener = true;
+ }
+ }
+
+ // Don't call callback in synchronized block because mListener.onQueryComplete may
+ // take long time to complete
+ if (shouldCallListener && mListener != null) {
+ addCallerInfoIntoCache(ci, directoryId);
+ mListener.onQueryComplete(token, cookie, ci);
+ }
+ }
+
+ private void addCallerInfoIntoCache(CallerInfo ci, long directoryId) {
+ CachedNumberLookupService cachedNumberLookupService =
+ PhoneNumberCache.get(mContext).getCachedNumberLookupService();
+ if (ci.contactExists && cachedNumberLookupService != null) {
+ // 1. Cache caller info
+ CachedContactInfo cachedContactInfo =
+ CallerInfoUtils.buildCachedContactInfo(cachedNumberLookupService, ci);
+ String directoryLabel = mContext.getString(R.string.directory_search_label);
+ cachedContactInfo.setDirectorySource(directoryLabel, directoryId);
+ cachedNumberLookupService.addContact(mContext, cachedContactInfo);
+
+ // 2. Cache photo
+ if (ci.contactDisplayPhotoUri != null && ci.normalizedNumber != null) {
+ try (InputStream in =
+ mContext.getContentResolver().openInputStream(ci.contactDisplayPhotoUri)) {
+ if (in != null) {
+ cachedNumberLookupService.addPhoto(mContext, ci.normalizedNumber, in);
+ }
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "failed to fetch directory contact photo", e);
+ }
+ }
+ }
+ }
+
+ public OnQueryCompleteListener newListener(long directoryId) {
+ return new DirectoryQueryCompleteListener(directoryId);
+ }
+
+ private class DirectoryQueryCompleteListener implements OnQueryCompleteListener {
+
+ private final long mDirectoryId;
+
+ DirectoryQueryCompleteListener(long directoryId) {
+ mDirectoryId = directoryId;
+ }
+
+ @Override
+ public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
+ mListener.onDataLoaded(token, cookie, ci);
+ }
+
+ @Override
+ public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
+ onDirectoryQueryComplete(token, cookie, ci, mDirectoryId);
+ }
+ }
+ }
+
+ /** Our own implementation of the AsyncQueryHandler. */
+ private static class CallerInfoAsyncQueryHandler extends AsyncQueryHandler {
+
+ /**
+ * The information relevant to each CallerInfo query. Each query may have multiple listeners, so
+ * each AsyncCursorInfo is associated with 2 or more CookieWrapper objects in the queue (one
+ * with a new query event, and one with a end event, with 0 or more additional listeners in
+ * between).
+ */
+ private Context mQueryContext;
+
+ private Uri mQueryUri;
+ private CallerInfo mCallerInfo;
+
+ /** Asynchronous query handler class for the contact / callerinfo object. */
+ private CallerInfoAsyncQueryHandler(Context context, Uri contactRef) {
+ super(context.getContentResolver());
+ this.mQueryContext = context;
+ this.mQueryUri = contactRef;
+ }
+
+ @Override
+ public void startQuery(
+ int token,
+ Object cookie,
+ Uri uri,
+ String[] projection,
+ String selection,
+ String[] selectionArgs,
+ String orderBy) {
+ if (DBG) {
+ // Show stack trace with the arguments.
+ Log.d(
+ LOG_TAG,
+ "InCall: startQuery: url="
+ + uri
+ + " projection=["
+ + Arrays.toString(projection)
+ + "]"
+ + " selection="
+ + selection
+ + " "
+ + " args=["
+ + Arrays.toString(selectionArgs)
+ + "]",
+ new RuntimeException("STACKTRACE"));
+ }
+ super.startQuery(token, cookie, uri, projection, selection, selectionArgs, orderBy);
+ }
+
+ @Override
+ protected Handler createHandler(Looper looper) {
+ return new CallerInfoWorkerHandler(looper);
+ }
+
+ /**
+ * Overrides onQueryComplete from AsyncQueryHandler.
+ *
+ * <p>This method takes into account the state of this class; we construct the CallerInfo object
+ * only once for each set of listeners. When the query thread has done its work and calls this
+ * method, we inform the remaining listeners in the queue, until we're out of listeners. Once we
+ * get the message indicating that we should expect no new listeners for this CallerInfo object,
+ * we release the AsyncCursorInfo back into the pool.
+ */
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ Log.d(this, "##### onQueryComplete() ##### query complete for token: " + token);
+
+ CookieWrapper cw = (CookieWrapper) cookie;
+
+ if (cw.listener != null) {
+ Log.d(
+ this,
+ "notifying listener: "
+ + cw.listener.getClass().toString()
+ + " for token: "
+ + token
+ + mCallerInfo);
+ cw.listener.onQueryComplete(token, cw.cookie, mCallerInfo);
+ }
+ mQueryContext = null;
+ mQueryUri = null;
+ mCallerInfo = null;
+ }
+
+ protected void updateData(int token, Object cookie, Cursor cursor) {
+ try {
+ Log.d(this, "##### updateData() ##### for token: " + token);
+
+ //get the cookie and notify the listener.
+ CookieWrapper cw = (CookieWrapper) cookie;
+ if (cw == null) {
+ // Normally, this should never be the case for calls originating
+ // from within this code.
+ // However, if there is any code that calls this method, we should
+ // check the parameters to make sure they're viable.
+ Log.d(this, "Cookie is null, ignoring onQueryComplete() request.");
+ return;
+ }
+
+ // check the token and if needed, create the callerinfo object.
+ if (mCallerInfo == null) {
+ if ((mQueryContext == null) || (mQueryUri == null)) {
+ throw new QueryPoolException(
+ "Bad context or query uri, or CallerInfoAsyncQuery already released.");
+ }
+
+ // adjust the callerInfo data as needed, and only if it was set from the
+ // initial query request.
+ // Change the callerInfo number ONLY if it is an emergency number or the
+ // voicemail number, and adjust other data (including photoResource)
+ // accordingly.
+ if (cw.event == EVENT_EMERGENCY_NUMBER) {
+ // Note we're setting the phone number here (refer to javadoc
+ // comments at the top of CallerInfo class).
+ mCallerInfo = new CallerInfo().markAsEmergency(mQueryContext);
+ } else if (cw.event == EVENT_VOICEMAIL_NUMBER) {
+ mCallerInfo = new CallerInfo().markAsVoiceMail(mQueryContext);
+ } else {
+ mCallerInfo = CallerInfo.getCallerInfo(mQueryContext, mQueryUri, cursor);
+ Log.d(this, "==> Got mCallerInfo: " + mCallerInfo);
+
+ CallerInfo newCallerInfo =
+ CallerInfo.doSecondaryLookupIfNecessary(mQueryContext, cw.number, mCallerInfo);
+ if (newCallerInfo != mCallerInfo) {
+ mCallerInfo = newCallerInfo;
+ Log.d(this, "#####async contact look up with numeric username" + mCallerInfo);
+ }
+
+ // Final step: look up the geocoded description.
+ if (ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION) {
+ // Note we do this only if we *don't* have a valid name (i.e. if
+ // no contacts matched the phone number of the incoming call),
+ // since that's the only case where the incoming-call UI cares
+ // about this field.
+ //
+ // (TODO: But if we ever want the UI to show the geoDescription
+ // even when we *do* match a contact, we'll need to either call
+ // updateGeoDescription() unconditionally here, or possibly add a
+ // new parameter to CallerInfoAsyncQuery.startQuery() to force
+ // the geoDescription field to be populated.)
+
+ if (TextUtils.isEmpty(mCallerInfo.name)) {
+ // Actually when no contacts match the incoming phone number,
+ // the CallerInfo object is totally blank here (i.e. no name
+ // *or* phoneNumber). So we need to pass in cw.number as
+ // a fallback number.
+ mCallerInfo.updateGeoDescription(mQueryContext, cw.number);
+ }
+ }
+
+ // Use the number entered by the user for display.
+ if (!TextUtils.isEmpty(cw.number)) {
+ mCallerInfo.phoneNumber = cw.number;
+ }
+ }
+
+ Log.d(this, "constructing CallerInfo object for token: " + token);
+
+ if (cw.listener != null) {
+ cw.listener.onDataLoaded(token, cw.cookie, mCallerInfo);
+ }
+ }
+
+ } finally {
+ // The cursor may have been closed in CallerInfo.getCallerInfo()
+ if (cursor != null && !cursor.isClosed()) {
+ cursor.close();
+ }
+ }
+ }
+
+ /**
+ * Our own query worker thread.
+ *
+ * <p>This thread handles the messages enqueued in the looper. The normal sequence of events is
+ * that a new query shows up in the looper queue, followed by 0 or more add listener requests,
+ * and then an end request. Of course, these requests can be interlaced with requests from other
+ * tokens, but is irrelevant to this handler since the handler has no state.
+ *
+ * <p>Note that we depend on the queue to keep things in order; in other words, the looper queue
+ * must be FIFO with respect to input from the synchronous startQuery calls and output to this
+ * handleMessage call.
+ *
+ * <p>This use of the queue is required because CallerInfo objects may be accessed multiple
+ * times before the query is complete. All accesses (listeners) must be queued up and informed
+ * in order when the query is complete.
+ */
+ protected class CallerInfoWorkerHandler extends WorkerHandler {
+
+ public CallerInfoWorkerHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+ CookieWrapper cw = (CookieWrapper) args.cookie;
+
+ if (cw == null) {
+ // Normally, this should never be the case for calls originating
+ // from within this code.
+ // However, if there is any code that this Handler calls (such as in
+ // super.handleMessage) that DOES place unexpected messages on the
+ // queue, then we need pass these messages on.
+ Log.d(
+ this,
+ "Unexpected command (CookieWrapper is null): "
+ + msg.what
+ + " ignored by CallerInfoWorkerHandler, passing onto parent.");
+
+ super.handleMessage(msg);
+ } else {
+ Log.d(
+ this,
+ "Processing event: "
+ + cw.event
+ + " token (arg1): "
+ + msg.arg1
+ + " command: "
+ + msg.what
+ + " query URI: "
+ + sanitizeUriToString(args.uri));
+
+ switch (cw.event) {
+ case EVENT_NEW_QUERY:
+ final ContentResolver resolver = mQueryContext.getContentResolver();
+
+ // This should never happen.
+ if (resolver == null) {
+ Log.e(this, "Content Resolver is null!");
+ return;
+ }
+ //start the sql command.
+ Cursor cursor;
+ try {
+ cursor =
+ resolver.query(
+ args.uri,
+ args.projection,
+ args.selection,
+ args.selectionArgs,
+ args.orderBy);
+ // Calling getCount() causes the cursor window to be filled,
+ // which will make the first access on the main thread a lot faster.
+ if (cursor != null) {
+ cursor.getCount();
+ }
+ } catch (Exception e) {
+ Log.e(this, "Exception thrown during handling EVENT_ARG_QUERY", e);
+ cursor = null;
+ }
+
+ args.result = cursor;
+ updateData(msg.arg1, cw, cursor);
+ break;
+
+ // shortcuts to avoid query for recognized numbers.
+ case EVENT_EMERGENCY_NUMBER:
+ case EVENT_VOICEMAIL_NUMBER:
+ case EVENT_ADD_LISTENER:
+ updateData(msg.arg1, cw, (Cursor) args.result);
+ break;
+ default:
+ }
+ Message reply = args.handler.obtainMessage(msg.what);
+ reply.obj = args;
+ reply.arg1 = msg.arg1;
+
+ reply.sendToTarget();
+ }
+ }
+ }
+ }
+}