• 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 com.android.dialer.dialpadview;
18 
19 import android.Manifest;
20 import android.annotation.SuppressLint;
21 import android.app.Activity;
22 import android.app.AlertDialog;
23 import android.app.DialogFragment;
24 import android.app.KeyguardManager;
25 import android.app.ProgressDialog;
26 import android.content.ActivityNotFoundException;
27 import android.content.ContentResolver;
28 import android.content.Context;
29 import android.content.DialogInterface;
30 import android.content.Intent;
31 import android.database.Cursor;
32 import android.graphics.Bitmap;
33 import android.graphics.Bitmap.Config;
34 import android.graphics.Color;
35 import android.net.Uri;
36 import android.provider.Settings;
37 import android.support.annotation.Nullable;
38 import android.support.annotation.VisibleForTesting;
39 import android.telecom.PhoneAccount;
40 import android.telecom.PhoneAccountHandle;
41 import android.telephony.PhoneNumberUtils;
42 import android.telephony.TelephonyManager;
43 import android.text.TextUtils;
44 import android.view.LayoutInflater;
45 import android.view.View;
46 import android.view.ViewGroup;
47 import android.view.ViewGroup.LayoutParams;
48 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
49 import android.view.WindowManager;
50 import android.widget.EditText;
51 import android.widget.ImageView;
52 import android.widget.TextView;
53 import android.widget.Toast;
54 import com.android.common.io.MoreCloseables;
55 import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler;
56 import com.android.contacts.common.util.ContactDisplayUtils;
57 import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment;
58 import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment.SelectPhoneAccountListener;
59 import com.android.contacts.common.widget.SelectPhoneAccountDialogOptionsUtil;
60 import com.android.dialer.common.Assert;
61 import com.android.dialer.common.LogUtil;
62 import com.android.dialer.compat.telephony.TelephonyManagerCompat;
63 import com.android.dialer.oem.MotorolaUtils;
64 import com.android.dialer.oem.TranssionUtils;
65 import com.android.dialer.telecom.TelecomUtil;
66 import com.android.dialer.util.PermissionsUtil;
67 import com.google.zxing.BarcodeFormat;
68 import com.google.zxing.MultiFormatWriter;
69 import com.google.zxing.WriterException;
70 import com.google.zxing.common.BitMatrix;
71 import java.util.Arrays;
72 import java.util.List;
73 import java.util.Locale;
74 
75 /**
76  * Helper class to listen for some magic character sequences that are handled specially by Dialer.
77  */
78 public class SpecialCharSequenceMgr {
79   private static final String TAG_SELECT_ACCT_FRAGMENT = "tag_select_acct_fragment";
80 
81   @VisibleForTesting static final String MMI_IMEI_DISPLAY = "*#06#";
82   private static final String MMI_REGULATORY_INFO_DISPLAY = "*#07#";
83   /** ***** This code is used to handle SIM Contact queries ***** */
84   private static final String ADN_PHONE_NUMBER_COLUMN_NAME = "number";
85 
86   private static final String ADN_NAME_COLUMN_NAME = "name";
87   private static final int ADN_QUERY_TOKEN = -1;
88 
89   /**
90    * Remembers the previous {@link QueryHandler} and cancel the operation when needed, to prevent
91    * possible crash.
92    *
93    * <p>QueryHandler may call {@link ProgressDialog#dismiss()} when the screen is already gone,
94    * which will cause the app crash. This variable enables the class to prevent the crash on {@link
95    * #cleanup()}.
96    *
97    * <p>TODO: Remove this and replace it (and {@link #cleanup()}) with better implementation. One
98    * complication is that we have SpecialCharSequenceMgr in Phone package too, which has *slightly*
99    * different implementation. Note that Phone package doesn't have this problem, so the class on
100    * Phone side doesn't have this functionality. Fundamental fix would be to have one shared
101    * implementation and resolve this corner case more gracefully.
102    */
103   private static QueryHandler previousAdnQueryHandler;
104 
105   /** This class is never instantiated. */
SpecialCharSequenceMgr()106   private SpecialCharSequenceMgr() {}
107 
handleChars(Context context, String input, EditText textField)108   public static boolean handleChars(Context context, String input, EditText textField) {
109     // get rid of the separators so that the string gets parsed correctly
110     String dialString = PhoneNumberUtils.stripSeparators(input);
111 
112     if (handleDeviceIdDisplay(context, dialString)
113         || handleRegulatoryInfoDisplay(context, dialString)
114         || handlePinEntry(context, dialString)
115         || handleAdnEntry(context, dialString, textField)
116         || handleSecretCode(context, dialString)) {
117       return true;
118     }
119 
120     if (MotorolaUtils.handleSpecialCharSequence(context, input)) {
121       return true;
122     }
123 
124     return false;
125   }
126 
127   /**
128    * Cleanup everything around this class. Must be run inside the main thread.
129    *
130    * <p>This should be called when the screen becomes background.
131    */
cleanup()132   public static void cleanup() {
133     Assert.isMainThread();
134 
135     if (previousAdnQueryHandler != null) {
136       previousAdnQueryHandler.cancel();
137       previousAdnQueryHandler = null;
138     }
139   }
140 
141   /**
142    * Handles secret codes to launch arbitrary activities in the form of
143    * *#*#<code>#*#* or *#<code_starting_with_number>#.
144    *
145    * @param context the context to use
146    * @param input the text to check for a secret code in
147    * @return true if a secret code was encountered and handled
148    */
handleSecretCode(Context context, String input)149   static boolean handleSecretCode(Context context, String input) {
150     // Secret code specific to OEMs should be handled first.
151     if (TranssionUtils.isTranssionSecretCode(input)) {
152       TranssionUtils.handleTranssionSecretCode(context, input);
153       return true;
154     }
155 
156     // Secret codes are accessed by dialing *#*#<code>#*#* or "*#<code_starting_with_number>#"
157     if (input.length() > 8 && input.startsWith("*#*#") && input.endsWith("#*#*")) {
158       String secretCode = input.substring(4, input.length() - 4);
159       TelephonyManagerCompat.handleSecretCode(context, secretCode);
160       return true;
161     }
162 
163     return false;
164   }
165 
166   /**
167    * Handle ADN requests by filling in the SIM contact number into the requested EditText.
168    *
169    * <p>This code works alongside the Asynchronous query handler {@link QueryHandler} and query
170    * cancel handler implemented in {@link SimContactQueryCookie}.
171    */
handleAdnEntry(Context context, String input, EditText textField)172   static boolean handleAdnEntry(Context context, String input, EditText textField) {
173     /* ADN entries are of the form "N(N)(N)#" */
174     TelephonyManager telephonyManager =
175         (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
176     if (telephonyManager == null
177         || telephonyManager.getPhoneType() != TelephonyManager.PHONE_TYPE_GSM) {
178       return false;
179     }
180 
181     // if the phone is keyguard-restricted, then just ignore this
182     // input.  We want to make sure that sim card contacts are NOT
183     // exposed unless the phone is unlocked, and this code can be
184     // accessed from the emergency dialer.
185     KeyguardManager keyguardManager =
186         (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
187     if (keyguardManager.inKeyguardRestrictedInputMode()) {
188       return false;
189     }
190 
191     int len = input.length();
192     if ((len > 1) && (len < 5) && (input.endsWith("#"))) {
193       try {
194         // get the ordinal number of the sim contact
195         final int index = Integer.parseInt(input.substring(0, len - 1));
196 
197         // The original code that navigated to a SIM Contacts list view did not
198         // highlight the requested contact correctly, a requirement for PTCRB
199         // certification.  This behaviour is consistent with the UI paradigm
200         // for touch-enabled lists, so it does not make sense to try to work
201         // around it.  Instead we fill in the the requested phone number into
202         // the dialer text field.
203 
204         // create the async query handler
205         final QueryHandler handler = new QueryHandler(context.getContentResolver());
206 
207         // create the cookie object
208         final SimContactQueryCookie sc =
209             new SimContactQueryCookie(index - 1, handler, ADN_QUERY_TOKEN);
210 
211         // setup the cookie fields
212         sc.contactNum = index - 1;
213         sc.setTextField(textField);
214 
215         // create the progress dialog
216         sc.progressDialog = new ProgressDialog(context);
217         sc.progressDialog.setTitle(R.string.simContacts_title);
218         sc.progressDialog.setMessage(context.getText(R.string.simContacts_emptyLoading));
219         sc.progressDialog.setIndeterminate(true);
220         sc.progressDialog.setCancelable(true);
221         sc.progressDialog.setOnCancelListener(sc);
222         sc.progressDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
223 
224         List<PhoneAccountHandle> subscriptionAccountHandles =
225             TelecomUtil.getSubscriptionPhoneAccounts(context);
226         Context applicationContext = context.getApplicationContext();
227         boolean hasUserSelectedDefault =
228             subscriptionAccountHandles.contains(
229                 TelecomUtil.getDefaultOutgoingPhoneAccount(
230                     applicationContext, PhoneAccount.SCHEME_TEL));
231 
232         if (subscriptionAccountHandles.size() <= 1 || hasUserSelectedDefault) {
233           Uri uri = TelecomUtil.getAdnUriForPhoneAccount(applicationContext, null);
234           handleAdnQuery(handler, sc, uri);
235         } else {
236           SelectPhoneAccountListener callback =
237               new HandleAdnEntryAccountSelectedCallback(applicationContext, handler, sc);
238           DialogFragment dialogFragment =
239               SelectPhoneAccountDialogFragment.newInstance(
240                   SelectPhoneAccountDialogOptionsUtil.builderWithAccounts(
241                           subscriptionAccountHandles)
242                       .build(),
243                   callback);
244           dialogFragment.show(((Activity) context).getFragmentManager(), TAG_SELECT_ACCT_FRAGMENT);
245         }
246 
247         return true;
248       } catch (NumberFormatException ex) {
249         // Ignore
250       }
251     }
252     return false;
253   }
254 
handleAdnQuery(QueryHandler handler, SimContactQueryCookie cookie, Uri uri)255   private static void handleAdnQuery(QueryHandler handler, SimContactQueryCookie cookie, Uri uri) {
256     if (handler == null || cookie == null || uri == null) {
257       LogUtil.w("SpecialCharSequenceMgr.handleAdnQuery", "queryAdn parameters incorrect");
258       return;
259     }
260 
261     // display the progress dialog
262     cookie.progressDialog.show();
263 
264     // run the query.
265     handler.startQuery(
266         ADN_QUERY_TOKEN,
267         cookie,
268         uri,
269         new String[] {ADN_PHONE_NUMBER_COLUMN_NAME},
270         null,
271         null,
272         null);
273 
274     if (previousAdnQueryHandler != null) {
275       // It is harmless to call cancel() even after the handler's gone.
276       previousAdnQueryHandler.cancel();
277     }
278     previousAdnQueryHandler = handler;
279   }
280 
handlePinEntry(final Context context, final String input)281   static boolean handlePinEntry(final Context context, final String input) {
282     if ((input.startsWith("**04") || input.startsWith("**05")) && input.endsWith("#")) {
283       List<PhoneAccountHandle> subscriptionAccountHandles =
284           TelecomUtil.getSubscriptionPhoneAccounts(context);
285       boolean hasUserSelectedDefault =
286           subscriptionAccountHandles.contains(
287               TelecomUtil.getDefaultOutgoingPhoneAccount(context, PhoneAccount.SCHEME_TEL));
288 
289       if (subscriptionAccountHandles.size() <= 1 || hasUserSelectedDefault) {
290         // Don't bring up the dialog for single-SIM or if the default outgoing account is
291         // a subscription account.
292         return TelecomUtil.handleMmi(context, input, null);
293       } else {
294         SelectPhoneAccountListener listener = new HandleMmiAccountSelectedCallback(context, input);
295 
296         DialogFragment dialogFragment =
297             SelectPhoneAccountDialogFragment.newInstance(
298                 SelectPhoneAccountDialogOptionsUtil.builderWithAccounts(subscriptionAccountHandles)
299                     .build(),
300                 listener);
301         dialogFragment.show(((Activity) context).getFragmentManager(), TAG_SELECT_ACCT_FRAGMENT);
302       }
303       return true;
304     }
305     return false;
306   }
307 
308   // TODO: Use TelephonyCapabilities.getDeviceIdLabel() to get the device id label instead of a
309   // hard-coded string.
310   @SuppressLint("HardwareIds")
handleDeviceIdDisplay(Context context, String input)311   static boolean handleDeviceIdDisplay(Context context, String input) {
312     if (!PermissionsUtil.hasPermission(context, Manifest.permission.READ_PHONE_STATE)) {
313       return false;
314     }
315     TelephonyManager telephonyManager =
316         (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
317 
318     if (telephonyManager != null && input.equals(MMI_IMEI_DISPLAY)) {
319       int labelResId =
320           (telephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM)
321               ? R.string.imei
322               : R.string.meid;
323 
324       View customView = LayoutInflater.from(context).inflate(R.layout.dialog_deviceids, null);
325       ViewGroup holder = customView.findViewById(R.id.deviceids_holder);
326 
327       if (TelephonyManagerCompat.getPhoneCount(telephonyManager) > 1) {
328         for (int slot = 0; slot < telephonyManager.getPhoneCount(); slot++) {
329           String deviceId = telephonyManager.getDeviceId(slot);
330           if (!TextUtils.isEmpty(deviceId)) {
331             addDeviceIdRow(
332                 holder,
333                 deviceId,
334                 /* showDecimal */
335                 context.getResources().getBoolean(R.bool.show_device_id_in_hex_and_decimal),
336                 /* showBarcode */ false);
337           }
338         }
339       } else {
340         addDeviceIdRow(
341             holder,
342             telephonyManager.getDeviceId(),
343             /* showDecimal */
344             context.getResources().getBoolean(R.bool.show_device_id_in_hex_and_decimal),
345             /* showBarcode */
346             context.getResources().getBoolean(R.bool.show_device_id_as_barcode));
347       }
348 
349       new AlertDialog.Builder(context)
350           .setTitle(labelResId)
351           .setView(customView)
352           .setPositiveButton(android.R.string.ok, null)
353           .setCancelable(false)
354           .show()
355           .getWindow()
356           .setLayout(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
357       return true;
358     }
359     return false;
360   }
361 
addDeviceIdRow( ViewGroup holder, String deviceId, boolean showDecimal, boolean showBarcode)362   private static void addDeviceIdRow(
363       ViewGroup holder, String deviceId, boolean showDecimal, boolean showBarcode) {
364     if (TextUtils.isEmpty(deviceId)) {
365       return;
366     }
367 
368     ViewGroup row =
369         (ViewGroup)
370             LayoutInflater.from(holder.getContext()).inflate(R.layout.row_deviceid, holder, false);
371     holder.addView(row);
372 
373     // Remove the check digit, if exists. This digit is a checksum of the ID.
374     // See https://en.wikipedia.org/wiki/International_Mobile_Equipment_Identity
375     // and https://en.wikipedia.org/wiki/Mobile_equipment_identifier
376     String hex = deviceId.length() == 15 ? deviceId.substring(0, 14) : deviceId;
377 
378     // If this is the valid length IMEI or MEID (14 digits), show it in all formats, otherwise fall
379     // back to just showing the raw hex
380     if (hex.length() == 14 && showDecimal) {
381       ((TextView) row.findViewById(R.id.deviceid_hex)).setText(hex);
382       ((TextView) row.findViewById(R.id.deviceid_dec)).setText(getDecimalFromHex(hex));
383       row.findViewById(R.id.deviceid_dec_label).setVisibility(View.VISIBLE);
384     } else {
385       row.findViewById(R.id.deviceid_hex_label).setVisibility(View.GONE);
386       ((TextView) row.findViewById(R.id.deviceid_hex)).setText(deviceId);
387     }
388 
389     final ImageView barcode = row.findViewById(R.id.deviceid_barcode);
390     if (showBarcode) {
391       // Wait until the layout pass has completed so we the barcode is measured before drawing. We
392       // do this by adding a layout listener and setting the bitmap after getting the callback.
393       barcode
394           .getViewTreeObserver()
395           .addOnGlobalLayoutListener(
396               new OnGlobalLayoutListener() {
397                 @Override
398                 public void onGlobalLayout() {
399                   barcode.getViewTreeObserver().removeOnGlobalLayoutListener(this);
400                   Bitmap barcodeBitmap =
401                       generateBarcode(hex, barcode.getWidth(), barcode.getHeight());
402                   if (barcodeBitmap != null) {
403                     barcode.setImageBitmap(barcodeBitmap);
404                   }
405                 }
406               });
407     } else {
408       barcode.setVisibility(View.GONE);
409     }
410   }
411 
getDecimalFromHex(String hex)412   private static String getDecimalFromHex(String hex) {
413     final String part1 = hex.substring(0, 8);
414     final String part2 = hex.substring(8);
415 
416     long dec1;
417     try {
418       dec1 = Long.parseLong(part1, 16);
419     } catch (NumberFormatException e) {
420       LogUtil.e("SpecialCharSequenceMgr.getDecimalFromHex", "unable to parse hex", e);
421       return "";
422     }
423 
424     final String manufacturerCode = String.format(Locale.US, "%010d", dec1);
425 
426     long dec2;
427     try {
428       dec2 = Long.parseLong(part2, 16);
429     } catch (NumberFormatException e) {
430       LogUtil.e("SpecialCharSequenceMgr.getDecimalFromHex", "unable to parse hex", e);
431       return "";
432     }
433 
434     final String serialNum = String.format(Locale.US, "%08d", dec2);
435 
436     StringBuilder builder = new StringBuilder(22);
437     builder
438         .append(manufacturerCode, 0, 5)
439         .append(' ')
440         .append(manufacturerCode, 5, manufacturerCode.length())
441         .append(' ')
442         .append(serialNum, 0, 4)
443         .append(' ')
444         .append(serialNum, 4, serialNum.length());
445     return builder.toString();
446   }
447 
448   /**
449    * This method generates a 2d barcode using the zxing library. Each pixel of the bitmap is either
450    * black or white painted vertically. We determine which color using the BitMatrix.get(x, y)
451    * method.
452    */
generateBarcode(String hex, int width, int height)453   private static Bitmap generateBarcode(String hex, int width, int height) {
454     MultiFormatWriter writer = new MultiFormatWriter();
455     String data = Uri.encode(hex);
456 
457     try {
458       BitMatrix bitMatrix = writer.encode(data, BarcodeFormat.CODE_128, width, 1);
459       Bitmap bitmap = Bitmap.createBitmap(bitMatrix.getWidth(), height, Config.RGB_565);
460 
461       for (int i = 0; i < bitMatrix.getWidth(); i++) {
462         // Paint columns of width 1
463         int[] column = new int[height];
464         Arrays.fill(column, bitMatrix.get(i, 0) ? Color.BLACK : Color.WHITE);
465         bitmap.setPixels(column, 0, 1, i, 0, 1, height);
466       }
467       return bitmap;
468     } catch (WriterException e) {
469       LogUtil.e("SpecialCharSequenceMgr.generateBarcode", "error generating barcode", e);
470     }
471     return null;
472   }
473 
handleRegulatoryInfoDisplay(Context context, String input)474   private static boolean handleRegulatoryInfoDisplay(Context context, String input) {
475     if (input.equals(MMI_REGULATORY_INFO_DISPLAY)) {
476       LogUtil.i(
477           "SpecialCharSequenceMgr.handleRegulatoryInfoDisplay", "sending intent to settings app");
478       Intent showRegInfoIntent = new Intent(Settings.ACTION_SHOW_REGULATORY_INFO);
479       try {
480         context.startActivity(showRegInfoIntent);
481       } catch (ActivityNotFoundException e) {
482         LogUtil.e(
483             "SpecialCharSequenceMgr.handleRegulatoryInfoDisplay", "startActivity() failed: ", e);
484       }
485       return true;
486     }
487     return false;
488   }
489 
490   public static class HandleAdnEntryAccountSelectedCallback extends SelectPhoneAccountListener {
491 
492     private final Context context;
493     private final QueryHandler queryHandler;
494     private final SimContactQueryCookie cookie;
495 
HandleAdnEntryAccountSelectedCallback( Context context, QueryHandler queryHandler, SimContactQueryCookie cookie)496     public HandleAdnEntryAccountSelectedCallback(
497         Context context, QueryHandler queryHandler, SimContactQueryCookie cookie) {
498       this.context = context;
499       this.queryHandler = queryHandler;
500       this.cookie = cookie;
501     }
502 
503     @Override
onPhoneAccountSelected( PhoneAccountHandle selectedAccountHandle, boolean setDefault, @Nullable String callId)504     public void onPhoneAccountSelected(
505         PhoneAccountHandle selectedAccountHandle, boolean setDefault, @Nullable String callId) {
506       Uri uri = TelecomUtil.getAdnUriForPhoneAccount(context, selectedAccountHandle);
507       handleAdnQuery(queryHandler, cookie, uri);
508       // TODO: Show error dialog if result isn't valid.
509     }
510   }
511 
512   public static class HandleMmiAccountSelectedCallback extends SelectPhoneAccountListener {
513 
514     private final Context context;
515     private final String input;
516 
HandleMmiAccountSelectedCallback(Context context, String input)517     public HandleMmiAccountSelectedCallback(Context context, String input) {
518       this.context = context.getApplicationContext();
519       this.input = input;
520     }
521 
522     @Override
onPhoneAccountSelected( PhoneAccountHandle selectedAccountHandle, boolean setDefault, @Nullable String callId)523     public void onPhoneAccountSelected(
524         PhoneAccountHandle selectedAccountHandle, boolean setDefault, @Nullable String callId) {
525       TelecomUtil.handleMmi(context, input, selectedAccountHandle);
526     }
527   }
528 
529   /**
530    * Cookie object that contains everything we need to communicate to the handler's onQuery
531    * Complete, as well as what we need in order to cancel the query (if requested).
532    *
533    * <p>Note, access to the textField field is going to be synchronized, because the user can
534    * request a cancel at any time through the UI.
535    */
536   private static class SimContactQueryCookie implements DialogInterface.OnCancelListener {
537 
538     public ProgressDialog progressDialog;
539     public int contactNum;
540 
541     // Used to identify the query request.
542     private int token;
543     private QueryHandler handler;
544 
545     // The text field we're going to update
546     private EditText textField;
547 
SimContactQueryCookie(int number, QueryHandler handler, int token)548     public SimContactQueryCookie(int number, QueryHandler handler, int token) {
549       contactNum = number;
550       this.handler = handler;
551       this.token = token;
552     }
553 
554     /** Synchronized getter for the EditText. */
getTextField()555     public synchronized EditText getTextField() {
556       return textField;
557     }
558 
559     /** Synchronized setter for the EditText. */
setTextField(EditText text)560     public synchronized void setTextField(EditText text) {
561       textField = text;
562     }
563 
564     /**
565      * Cancel the ADN query by stopping the operation and signaling the cookie that a cancel request
566      * is made.
567      */
568     @Override
onCancel(DialogInterface dialog)569     public synchronized void onCancel(DialogInterface dialog) {
570       // close the progress dialog
571       if (progressDialog != null) {
572         progressDialog.dismiss();
573       }
574 
575       // setting the textfield to null ensures that the UI does NOT get
576       // updated.
577       textField = null;
578 
579       // Cancel the operation if possible.
580       handler.cancelOperation(token);
581     }
582   }
583 
584   /**
585    * Asynchronous query handler that services requests to look up ADNs
586    *
587    * <p>Queries originate from {@link #handleAdnEntry}.
588    */
589   private static class QueryHandler extends NoNullCursorAsyncQueryHandler {
590 
591     private boolean canceled;
592 
QueryHandler(ContentResolver cr)593     public QueryHandler(ContentResolver cr) {
594       super(cr);
595     }
596 
597     /** Override basic onQueryComplete to fill in the textfield when we're handed the ADN cursor. */
598     @Override
onNotNullableQueryComplete(int token, Object cookie, Cursor c)599     protected void onNotNullableQueryComplete(int token, Object cookie, Cursor c) {
600       try {
601         previousAdnQueryHandler = null;
602         if (canceled) {
603           return;
604         }
605 
606         SimContactQueryCookie sc = (SimContactQueryCookie) cookie;
607 
608         // close the progress dialog.
609         sc.progressDialog.dismiss();
610 
611         // get the EditText to update or see if the request was cancelled.
612         EditText text = sc.getTextField();
613 
614         // if the TextView is valid, and the cursor is valid and positionable on the
615         // Nth number, then we update the text field and display a toast indicating the
616         // caller name.
617         if ((c != null) && (text != null) && (c.moveToPosition(sc.contactNum))) {
618           String name = c.getString(c.getColumnIndexOrThrow(ADN_NAME_COLUMN_NAME));
619           String number = c.getString(c.getColumnIndexOrThrow(ADN_PHONE_NUMBER_COLUMN_NAME));
620 
621           // fill the text in.
622           text.getText().replace(0, 0, number);
623 
624           // display the name as a toast
625           Context context = sc.progressDialog.getContext();
626           CharSequence msg =
627               ContactDisplayUtils.getTtsSpannedPhoneNumber(
628                   context.getResources(), R.string.menu_callNumber, name);
629           Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
630         }
631       } finally {
632         MoreCloseables.closeQuietly(c);
633       }
634     }
635 
cancel()636     public void cancel() {
637       canceled = true;
638       // Ask AsyncQueryHandler to cancel the whole request. This will fail when the query is
639       // already started.
640       cancelOperation(ADN_QUERY_TOKEN);
641     }
642   }
643 }
644