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