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