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