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