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