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