Home | History | Annotate | Download | only in dialer
      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;
     18 
     19 import android.app.Activity;
     20 import android.app.AlertDialog;
     21 import android.app.KeyguardManager;
     22 import android.app.ProgressDialog;
     23 import android.content.ActivityNotFoundException;
     24 import android.content.ContentResolver;
     25 import android.content.Context;
     26 import android.content.DialogInterface;
     27 import android.content.Intent;
     28 import android.database.Cursor;
     29 import android.net.Uri;
     30 import android.os.Looper;
     31 import android.provider.Settings;
     32 import android.telecom.PhoneAccountHandle;
     33 import android.telecom.TelecomManager;
     34 import android.telephony.PhoneNumberUtils;
     35 import android.telephony.TelephonyManager;
     36 import android.util.Log;
     37 import android.view.WindowManager;
     38 import android.widget.EditText;
     39 import android.widget.Toast;
     40 
     41 import com.android.common.io.MoreCloseables;
     42 import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler;
     43 import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment;
     44 import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment.SelectPhoneAccountListener;
     45 import com.android.dialer.calllog.PhoneAccountUtils;
     46 
     47 import java.util.Arrays;
     48 import java.util.ArrayList;
     49 import java.util.List;
     50 
     51 /**
     52  * Helper class to listen for some magic character sequences
     53  * that are handled specially by the dialer.
     54  *
     55  * Note the Phone app also handles these sequences too (in a couple of
     56  * relatively obscure places in the UI), so there's a separate version of
     57  * this class under apps/Phone.
     58  *
     59  * TODO: there's lots of duplicated code between this class and the
     60  * corresponding class under apps/Phone.  Let's figure out a way to
     61  * unify these two classes (in the framework? in a common shared library?)
     62  */
     63 public class SpecialCharSequenceMgr {
     64     private static final String TAG = "SpecialCharSequenceMgr";
     65 
     66     private static final String SECRET_CODE_ACTION = "android.provider.Telephony.SECRET_CODE";
     67     private static final String MMI_IMEI_DISPLAY = "*#06#";
     68     private static final String MMI_REGULATORY_INFO_DISPLAY = "*#07#";
     69 
     70     /**
     71      * Remembers the previous {@link QueryHandler} and cancel the operation when needed, to
     72      * prevent possible crash.
     73      *
     74      * QueryHandler may call {@link ProgressDialog#dismiss()} when the screen is already gone,
     75      * which will cause the app crash. This variable enables the class to prevent the crash
     76      * on {@link #cleanup()}.
     77      *
     78      * TODO: Remove this and replace it (and {@link #cleanup()}) with better implementation.
     79      * One complication is that we have SpecialCharSequenceMgr in Phone package too, which has
     80      * *slightly* different implementation. Note that Phone package doesn't have this problem,
     81      * so the class on Phone side doesn't have this functionality.
     82      * Fundamental fix would be to have one shared implementation and resolve this corner case more
     83      * gracefully.
     84      */
     85     private static QueryHandler sPreviousAdnQueryHandler;
     86 
     87     /** This class is never instantiated. */
     88     private SpecialCharSequenceMgr() {
     89     }
     90 
     91     public static boolean handleChars(Context context, String input, EditText textField) {
     92         //get rid of the separators so that the string gets parsed correctly
     93         String dialString = PhoneNumberUtils.stripSeparators(input);
     94 
     95         if (handleDeviceIdDisplay(context, dialString)
     96                 || handleRegulatoryInfoDisplay(context, dialString)
     97                 || handlePinEntry(context, dialString)
     98                 || handleAdnEntry(context, dialString, textField)
     99                 || handleSecretCode(context, dialString)) {
    100             return true;
    101         }
    102 
    103         return false;
    104     }
    105 
    106     /**
    107      * Cleanup everything around this class. Must be run inside the main thread.
    108      *
    109      * This should be called when the screen becomes background.
    110      */
    111     public static void cleanup() {
    112         if (Looper.myLooper() != Looper.getMainLooper()) {
    113             Log.wtf(TAG, "cleanup() is called outside the main thread");
    114             return;
    115         }
    116 
    117         if (sPreviousAdnQueryHandler != null) {
    118             sPreviousAdnQueryHandler.cancel();
    119             sPreviousAdnQueryHandler = null;
    120         }
    121     }
    122 
    123     /**
    124      * Handles secret codes to launch arbitrary activities in the form of *#*#<code>#*#*.
    125      * If a secret code is encountered an Intent is started with the android_secret_code://<code>
    126      * URI.
    127      *
    128      * @param context the context to use
    129      * @param input the text to check for a secret code in
    130      * @return true if a secret code was encountered
    131      */
    132     static boolean handleSecretCode(Context context, String input) {
    133         // Secret codes are in the form *#*#<code>#*#*
    134         int len = input.length();
    135         if (len > 8 && input.startsWith("*#*#") && input.endsWith("#*#*")) {
    136             final Intent intent = new Intent(SECRET_CODE_ACTION,
    137                     Uri.parse("android_secret_code://" + input.substring(4, len - 4)));
    138             context.sendBroadcast(intent);
    139             return true;
    140         }
    141 
    142         return false;
    143     }
    144 
    145     /**
    146      * Handle ADN requests by filling in the SIM contact number into the requested
    147      * EditText.
    148      *
    149      * This code works alongside the Asynchronous query handler {@link QueryHandler}
    150      * and query cancel handler implemented in {@link SimContactQueryCookie}.
    151      */
    152     static boolean handleAdnEntry(final Context context, String input, EditText textField) {
    153         /* ADN entries are of the form "N(N)(N)#" */
    154 
    155         TelephonyManager telephonyManager =
    156                 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    157         if (telephonyManager == null
    158                 || telephonyManager.getPhoneType() != TelephonyManager.PHONE_TYPE_GSM) {
    159             return false;
    160         }
    161 
    162         // if the phone is keyguard-restricted, then just ignore this
    163         // input.  We want to make sure that sim card contacts are NOT
    164         // exposed unless the phone is unlocked, and this code can be
    165         // accessed from the emergency dialer.
    166         KeyguardManager keyguardManager =
    167                 (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
    168         if (keyguardManager.inKeyguardRestrictedInputMode()) {
    169             return false;
    170         }
    171 
    172         int len = input.length();
    173         if ((len > 1) && (len < 5) && (input.endsWith("#"))) {
    174             try {
    175                 // get the ordinal number of the sim contact
    176                 final int index = Integer.parseInt(input.substring(0, len-1));
    177 
    178                 // The original code that navigated to a SIM Contacts list view did not
    179                 // highlight the requested contact correctly, a requirement for PTCRB
    180                 // certification.  This behaviour is consistent with the UI paradigm
    181                 // for touch-enabled lists, so it does not make sense to try to work
    182                 // around it.  Instead we fill in the the requested phone number into
    183                 // the dialer text field.
    184 
    185                 // create the async query handler
    186                 final QueryHandler handler = new QueryHandler (context.getContentResolver());
    187 
    188                 // create the cookie object
    189                 final SimContactQueryCookie sc = new SimContactQueryCookie(index - 1, handler,
    190                         ADN_QUERY_TOKEN);
    191 
    192                 // setup the cookie fields
    193                 sc.contactNum = index - 1;
    194                 sc.setTextField(textField);
    195 
    196                 // create the progress dialog
    197                 sc.progressDialog = new ProgressDialog(context);
    198                 sc.progressDialog.setTitle(R.string.simContacts_title);
    199                 sc.progressDialog.setMessage(context.getText(R.string.simContacts_emptyLoading));
    200                 sc.progressDialog.setIndeterminate(true);
    201                 sc.progressDialog.setCancelable(true);
    202                 sc.progressDialog.setOnCancelListener(sc);
    203                 sc.progressDialog.getWindow().addFlags(
    204                         WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
    205 
    206                 final TelecomManager telecomManager =
    207                         (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
    208                 List<PhoneAccountHandle> subscriptionAccountHandles =
    209                         PhoneAccountUtils.getSubscriptionPhoneAccounts(context);
    210 
    211                 boolean hasUserSelectedDefault = subscriptionAccountHandles.contains(
    212                         telecomManager.getUserSelectedOutgoingPhoneAccount());
    213 
    214                 if (subscriptionAccountHandles.size() == 1 || hasUserSelectedDefault) {
    215                     Uri uri = telecomManager.getAdnUriForPhoneAccount(null);
    216                     handleAdnQuery(handler, sc, uri);
    217                 } else if (subscriptionAccountHandles.size() > 1){
    218                     SelectPhoneAccountListener listener = new SelectPhoneAccountListener() {
    219                         @Override
    220                         public void onPhoneAccountSelected(PhoneAccountHandle selectedAccountHandle,
    221                                 boolean setDefault) {
    222                             Uri uri =
    223                                     telecomManager.getAdnUriForPhoneAccount(selectedAccountHandle);
    224                             handleAdnQuery(handler, sc, uri);
    225                             //TODO: show error dialog if result isn't valid
    226                         }
    227                         @Override
    228                         public void onDialogDismissed() {}
    229                     };
    230 
    231                     SelectPhoneAccountDialogFragment.showAccountDialog(
    232                             ((Activity) context).getFragmentManager(), subscriptionAccountHandles,
    233                             listener);
    234                 } else {
    235                     return false;
    236                 }
    237 
    238                 return true;
    239             } catch (NumberFormatException ex) {
    240                 // Ignore
    241             }
    242         }
    243         return false;
    244     }
    245 
    246     private static void handleAdnQuery(QueryHandler handler, SimContactQueryCookie cookie,
    247             Uri uri) {
    248         if (handler == null || cookie == null || uri == null) {
    249             Log.w(TAG, "queryAdn parameters incorrect");
    250             return;
    251         }
    252 
    253         // display the progress dialog
    254         cookie.progressDialog.show();
    255 
    256         // run the query.
    257         handler.startQuery(ADN_QUERY_TOKEN, cookie, uri, new String[]{ADN_PHONE_NUMBER_COLUMN_NAME},
    258                 null, null, null);
    259 
    260         if (sPreviousAdnQueryHandler != null) {
    261             // It is harmless to call cancel() even after the handler's gone.
    262             sPreviousAdnQueryHandler.cancel();
    263         }
    264         sPreviousAdnQueryHandler = handler;
    265     }
    266 
    267     static boolean handlePinEntry(Context context, final String input) {
    268         if ((input.startsWith("**04") || input.startsWith("**05")) && input.endsWith("#")) {
    269             final TelecomManager telecomManager =
    270                     (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
    271             List<PhoneAccountHandle> subscriptionAccountHandles =
    272                     PhoneAccountUtils.getSubscriptionPhoneAccounts(context);
    273             boolean hasUserSelectedDefault = subscriptionAccountHandles.contains(
    274                     telecomManager.getUserSelectedOutgoingPhoneAccount());
    275 
    276             if (subscriptionAccountHandles.size() == 1 || hasUserSelectedDefault) {
    277                 // Don't bring up the dialog for single-SIM or if the default outgoing account is
    278                 // a subscription account.
    279                 return telecomManager.handleMmi(input);
    280             } else if (subscriptionAccountHandles.size() > 1){
    281                 SelectPhoneAccountListener listener = new SelectPhoneAccountListener() {
    282                     @Override
    283                     public void onPhoneAccountSelected(PhoneAccountHandle selectedAccountHandle,
    284                             boolean setDefault) {
    285                         telecomManager.handleMmi(selectedAccountHandle, input);
    286                         //TODO: show error dialog if result isn't valid
    287                     }
    288                     @Override
    289                     public void onDialogDismissed() {}
    290                 };
    291 
    292                 SelectPhoneAccountDialogFragment.showAccountDialog(
    293                         ((Activity) context).getFragmentManager(), subscriptionAccountHandles,
    294                         listener);
    295             }
    296             return true;
    297         }
    298         return false;
    299     }
    300 
    301     // TODO: Use TelephonyCapabilities.getDeviceIdLabel() to get the device id label instead of a
    302     // hard-coded string.
    303     static boolean handleDeviceIdDisplay(Context context, String input) {
    304         TelephonyManager telephonyManager =
    305                 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    306 
    307         if (telephonyManager != null && input.equals(MMI_IMEI_DISPLAY)) {
    308             int labelResId = (telephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM) ?
    309                     R.string.imei : R.string.meid;
    310 
    311             List<String> deviceIds = new ArrayList<String>();
    312             for (int slot = 0; slot < telephonyManager.getPhoneCount(); slot++) {
    313                 deviceIds.add(telephonyManager.getDeviceId(slot));
    314             }
    315 
    316             AlertDialog alert = new AlertDialog.Builder(context)
    317                     .setTitle(labelResId)
    318                     .setItems(deviceIds.toArray(new String[deviceIds.size()]), null)
    319                     .setPositiveButton(R.string.ok, null)
    320                     .setCancelable(false)
    321                     .show();
    322             return true;
    323         }
    324         return false;
    325     }
    326 
    327     private static boolean handleRegulatoryInfoDisplay(Context context, String input) {
    328         if (input.equals(MMI_REGULATORY_INFO_DISPLAY)) {
    329             Log.d(TAG, "handleRegulatoryInfoDisplay() sending intent to settings app");
    330             Intent showRegInfoIntent = new Intent(Settings.ACTION_SHOW_REGULATORY_INFO);
    331             try {
    332                 context.startActivity(showRegInfoIntent);
    333             } catch (ActivityNotFoundException e) {
    334                 Log.e(TAG, "startActivity() failed: " + e);
    335             }
    336             return true;
    337         }
    338         return false;
    339     }
    340 
    341     /*******
    342      * This code is used to handle SIM Contact queries
    343      *******/
    344     private static final String ADN_PHONE_NUMBER_COLUMN_NAME = "number";
    345     private static final String ADN_NAME_COLUMN_NAME = "name";
    346     private static final int ADN_QUERY_TOKEN = -1;
    347 
    348     /**
    349      * Cookie object that contains everything we need to communicate to the
    350      * handler's onQuery Complete, as well as what we need in order to cancel
    351      * the query (if requested).
    352      *
    353      * Note, access to the textField field is going to be synchronized, because
    354      * the user can request a cancel at any time through the UI.
    355      */
    356     private static class SimContactQueryCookie implements DialogInterface.OnCancelListener{
    357         public ProgressDialog progressDialog;
    358         public int contactNum;
    359 
    360         // Used to identify the query request.
    361         private int mToken;
    362         private QueryHandler mHandler;
    363 
    364         // The text field we're going to update
    365         private EditText textField;
    366 
    367         public SimContactQueryCookie(int number, QueryHandler handler, int token) {
    368             contactNum = number;
    369             mHandler = handler;
    370             mToken = token;
    371         }
    372 
    373         /**
    374          * Synchronized getter for the EditText.
    375          */
    376         public synchronized EditText getTextField() {
    377             return textField;
    378         }
    379 
    380         /**
    381          * Synchronized setter for the EditText.
    382          */
    383         public synchronized void setTextField(EditText text) {
    384             textField = text;
    385         }
    386 
    387         /**
    388          * Cancel the ADN query by stopping the operation and signaling
    389          * the cookie that a cancel request is made.
    390          */
    391         public synchronized void onCancel(DialogInterface dialog) {
    392             // close the progress dialog
    393             if (progressDialog != null) {
    394                 progressDialog.dismiss();
    395             }
    396 
    397             // setting the textfield to null ensures that the UI does NOT get
    398             // updated.
    399             textField = null;
    400 
    401             // Cancel the operation if possible.
    402             mHandler.cancelOperation(mToken);
    403         }
    404     }
    405 
    406     /**
    407      * Asynchronous query handler that services requests to look up ADNs
    408      *
    409      * Queries originate from {@link #handleAdnEntry}.
    410      */
    411     private static class QueryHandler extends NoNullCursorAsyncQueryHandler {
    412 
    413         private boolean mCanceled;
    414 
    415         public QueryHandler(ContentResolver cr) {
    416             super(cr);
    417         }
    418 
    419         /**
    420          * Override basic onQueryComplete to fill in the textfield when
    421          * we're handed the ADN cursor.
    422          */
    423         @Override
    424         protected void onNotNullableQueryComplete(int token, Object cookie, Cursor c) {
    425             try {
    426                 sPreviousAdnQueryHandler = null;
    427                 if (mCanceled) {
    428                     return;
    429                 }
    430 
    431                 SimContactQueryCookie sc = (SimContactQueryCookie) cookie;
    432 
    433                 // close the progress dialog.
    434                 sc.progressDialog.dismiss();
    435 
    436                 // get the EditText to update or see if the request was cancelled.
    437                 EditText text = sc.getTextField();
    438 
    439                 // if the TextView is valid, and the cursor is valid and positionable on the
    440                 // Nth number, then we update the text field and display a toast indicating the
    441                 // caller name.
    442                 if ((c != null) && (text != null) && (c.moveToPosition(sc.contactNum))) {
    443                     String name = c.getString(c.getColumnIndexOrThrow(ADN_NAME_COLUMN_NAME));
    444                     String number =
    445                             c.getString(c.getColumnIndexOrThrow(ADN_PHONE_NUMBER_COLUMN_NAME));
    446 
    447                     // fill the text in.
    448                     text.getText().replace(0, 0, number);
    449 
    450                     // display the name as a toast
    451                     Context context = sc.progressDialog.getContext();
    452                     name = context.getString(R.string.menu_callNumber, name);
    453                     Toast.makeText(context, name, Toast.LENGTH_SHORT)
    454                         .show();
    455                 }
    456             } finally {
    457                 MoreCloseables.closeQuietly(c);
    458             }
    459         }
    460 
    461         public void cancel() {
    462             mCanceled = true;
    463             // Ask AsyncQueryHandler to cancel the whole request. This will fail when the query is
    464             // already started.
    465             cancelOperation(ADN_QUERY_TOKEN);
    466         }
    467     }
    468 }
    469