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