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