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