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