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