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