1 /* 2 * Copyright (C) 2011 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.dialpadview; 18 19 import android.app.Activity; 20 import android.app.AlertDialog; 21 import android.app.Dialog; 22 import android.app.DialogFragment; 23 import android.app.Fragment; 24 import android.content.BroadcastReceiver; 25 import android.content.ContentResolver; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.IntentFilter; 29 import android.content.res.Configuration; 30 import android.content.res.Resources; 31 import android.database.Cursor; 32 import android.graphics.Bitmap; 33 import android.graphics.BitmapFactory; 34 import android.media.AudioManager; 35 import android.media.ToneGenerator; 36 import android.net.Uri; 37 import android.os.Bundle; 38 import android.os.Trace; 39 import android.provider.Contacts.People; 40 import android.provider.Contacts.Phones; 41 import android.provider.Contacts.PhonesColumns; 42 import android.provider.Settings; 43 import android.support.annotation.NonNull; 44 import android.support.annotation.Nullable; 45 import android.support.annotation.VisibleForTesting; 46 import android.support.design.widget.FloatingActionButton; 47 import android.telecom.PhoneAccount; 48 import android.telecom.PhoneAccountHandle; 49 import android.telephony.PhoneNumberFormattingTextWatcher; 50 import android.telephony.PhoneNumberUtils; 51 import android.telephony.TelephonyManager; 52 import android.text.Editable; 53 import android.text.Selection; 54 import android.text.TextUtils; 55 import android.text.TextWatcher; 56 import android.util.AttributeSet; 57 import android.view.HapticFeedbackConstants; 58 import android.view.KeyEvent; 59 import android.view.LayoutInflater; 60 import android.view.Menu; 61 import android.view.MenuItem; 62 import android.view.View; 63 import android.view.ViewGroup; 64 import android.view.animation.Animation; 65 import android.view.animation.Animation.AnimationListener; 66 import android.view.animation.AnimationUtils; 67 import android.widget.AdapterView; 68 import android.widget.BaseAdapter; 69 import android.widget.EditText; 70 import android.widget.ImageView; 71 import android.widget.ListView; 72 import android.widget.PopupMenu; 73 import android.widget.RelativeLayout; 74 import android.widget.TextView; 75 import com.android.contacts.common.dialog.CallSubjectDialog; 76 import com.android.contacts.common.util.StopWatch; 77 import com.android.dialer.animation.AnimUtils; 78 import com.android.dialer.callintent.CallInitiationType; 79 import com.android.dialer.callintent.CallIntentBuilder; 80 import com.android.dialer.common.Assert; 81 import com.android.dialer.common.FragmentUtils; 82 import com.android.dialer.common.LogUtil; 83 import com.android.dialer.common.concurrent.DialerExecutor; 84 import com.android.dialer.common.concurrent.DialerExecutor.Worker; 85 import com.android.dialer.common.concurrent.DialerExecutorComponent; 86 import com.android.dialer.common.concurrent.ThreadUtil; 87 import com.android.dialer.location.GeoUtil; 88 import com.android.dialer.logging.UiAction; 89 import com.android.dialer.oem.MotorolaUtils; 90 import com.android.dialer.performancereport.PerformanceReport; 91 import com.android.dialer.phonenumberutil.PhoneNumberHelper; 92 import com.android.dialer.precall.PreCall; 93 import com.android.dialer.proguard.UsedByReflection; 94 import com.android.dialer.telecom.TelecomUtil; 95 import com.android.dialer.util.CallUtil; 96 import com.android.dialer.util.PermissionsUtil; 97 import com.android.dialer.util.ViewUtil; 98 import com.android.dialer.widget.FloatingActionButtonController; 99 import com.google.common.base.Ascii; 100 import com.google.common.base.Optional; 101 import java.util.HashSet; 102 import java.util.List; 103 import java.util.regex.Matcher; 104 import java.util.regex.Pattern; 105 106 /** Fragment that displays a twelve-key phone dialpad. */ 107 public class DialpadFragment extends Fragment 108 implements View.OnClickListener, 109 View.OnLongClickListener, 110 View.OnKeyListener, 111 AdapterView.OnItemClickListener, 112 TextWatcher, 113 PopupMenu.OnMenuItemClickListener, 114 DialpadKeyButton.OnPressedListener { 115 116 private static final String TAG = "DialpadFragment"; 117 private static final String EMPTY_NUMBER = ""; 118 private static final char PAUSE = ','; 119 private static final char WAIT = ';'; 120 /** The length of DTMF tones in milliseconds */ 121 private static final int TONE_LENGTH_MS = 150; 122 123 private static final int TONE_LENGTH_INFINITE = -1; 124 /** The DTMF tone volume relative to other sounds in the stream */ 125 private static final int TONE_RELATIVE_VOLUME = 80; 126 /** Stream type used to play the DTMF tones off call, and mapped to the volume control keys */ 127 private static final int DIAL_TONE_STREAM_TYPE = AudioManager.STREAM_DTMF; 128 /** Identifier for the "Add Call" intent extra. */ 129 private static final String ADD_CALL_MODE_KEY = "add_call_mode"; 130 /** 131 * Identifier for intent extra for sending an empty Flash message for CDMA networks. This message 132 * is used by the network to simulate a press/depress of the "hookswitch" of a landline phone. Aka 133 * "empty flash". 134 * 135 * <p>TODO: Using an intent extra to tell the phone to send this flash is a temporary measure. To 136 * be replaced with an Telephony/TelecomManager call in the future. TODO: Keep in sync with the 137 * string defined in OutgoingCallBroadcaster.java in Phone app until this is replaced with the 138 * Telephony/Telecom API. 139 */ 140 private static final String EXTRA_SEND_EMPTY_FLASH = "com.android.phone.extra.SEND_EMPTY_FLASH"; 141 142 private static final String PREF_DIGITS_FILLED_BY_INTENT = "pref_digits_filled_by_intent"; 143 private static final String PREF_IS_DIALPAD_SLIDE_OUT = "pref_is_dialpad_slide_out"; 144 145 private static Optional<String> currentCountryIsoForTesting = Optional.absent(); 146 147 private final Object toneGeneratorLock = new Object(); 148 /** Set of dialpad keys that are currently being pressed */ 149 private final HashSet<View> pressedDialpadKeys = new HashSet<>(12); 150 151 private OnDialpadQueryChangedListener dialpadQueryListener; 152 private DialpadView dialpadView; 153 private EditText digits; 154 private int dialpadSlideInDuration; 155 /** Remembers if we need to clear digits field when the screen is completely gone. */ 156 private boolean clearDigitsOnStop; 157 158 private View overflowMenuButton; 159 private PopupMenu overflowPopupMenu; 160 private View delete; 161 private ToneGenerator toneGenerator; 162 private FloatingActionButtonController floatingActionButtonController; 163 private FloatingActionButton floatingActionButton; 164 private ListView dialpadChooser; 165 private DialpadChooserAdapter dialpadChooserAdapter; 166 /** Regular expression prohibiting manual phone call. Can be empty, which means "no rule". */ 167 private String prohibitedPhoneNumberRegexp; 168 169 private PseudoEmergencyAnimator pseudoEmergencyAnimator; 170 private String lastNumberDialed = EMPTY_NUMBER; 171 172 // determines if we want to playback local DTMF tones. 173 private boolean dTMFToneEnabled; 174 private CallStateReceiver callStateReceiver; 175 private boolean wasEmptyBeforeTextChange; 176 /** 177 * This field is set to true while processing an incoming DIAL intent, in order to make sure that 178 * SpecialCharSequenceMgr actions can be triggered by user input but *not* by a tel: URI passed by 179 * some other app. It will be set to false when all digits are cleared. 180 */ 181 private boolean digitsFilledByIntent; 182 183 private boolean startedFromNewIntent = false; 184 private boolean firstLaunch = false; 185 private boolean animate = false; 186 187 private boolean isLayoutRtl; 188 private boolean isLandscape; 189 190 private DialerExecutor<String> initPhoneNumberFormattingTextWatcherExecutor; 191 private boolean isDialpadSlideUp; 192 193 /** 194 * Determines whether an add call operation is requested. 195 * 196 * @param intent The intent. 197 * @return {@literal true} if add call operation was requested. {@literal false} otherwise. 198 */ 199 public static boolean isAddCallMode(Intent intent) { 200 if (intent == null) { 201 return false; 202 } 203 final String action = intent.getAction(); 204 if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) { 205 // see if we are "adding a call" from the InCallScreen; false by default. 206 return intent.getBooleanExtra(ADD_CALL_MODE_KEY, false); 207 } else { 208 return false; 209 } 210 } 211 212 /** 213 * Format the provided string of digits into one that represents a properly formatted phone 214 * number. 215 * 216 * @param dialString String of characters to format 217 * @param normalizedNumber the E164 format number whose country code is used if the given 218 * phoneNumber doesn't have the country code. 219 * @param countryIso The country code representing the format to use if the provided normalized 220 * number is null or invalid. 221 * @return the provided string of digits as a formatted phone number, retaining any post-dial 222 * portion of the string. 223 */ 224 String getFormattedDigits(String dialString, String normalizedNumber, String countryIso) { 225 String number = PhoneNumberUtils.extractNetworkPortion(dialString); 226 // Also retrieve the post dial portion of the provided data, so that the entire dial 227 // string can be reconstituted later. 228 final String postDial = PhoneNumberUtils.extractPostDialPortion(dialString); 229 230 if (TextUtils.isEmpty(number)) { 231 return postDial; 232 } 233 234 number = PhoneNumberHelper.formatNumber(getContext(), number, normalizedNumber, countryIso); 235 236 if (TextUtils.isEmpty(postDial)) { 237 return number; 238 } 239 240 return number.concat(postDial); 241 } 242 243 /** 244 * Returns true of the newDigit parameter can be added at the current selection point, otherwise 245 * returns false. Only prevents input of WAIT and PAUSE digits at an unsupported position. Fails 246 * early if start == -1 or start is larger than end. 247 */ 248 @VisibleForTesting 249 /* package */ static boolean canAddDigit(CharSequence digits, int start, int end, char newDigit) { 250 if (newDigit != WAIT && newDigit != PAUSE) { 251 throw new IllegalArgumentException( 252 "Should not be called for anything other than PAUSE & WAIT"); 253 } 254 255 // False if no selection, or selection is reversed (end < start) 256 if (start == -1 || end < start) { 257 return false; 258 } 259 260 // unsupported selection-out-of-bounds state 261 if (start > digits.length() || end > digits.length()) { 262 return false; 263 } 264 265 // Special digit cannot be the first digit 266 if (start == 0) { 267 return false; 268 } 269 270 if (newDigit == WAIT) { 271 // preceding char is ';' (WAIT) 272 if (digits.charAt(start - 1) == WAIT) { 273 return false; 274 } 275 276 // next char is ';' (WAIT) 277 if ((digits.length() > end) && (digits.charAt(end) == WAIT)) { 278 return false; 279 } 280 } 281 282 return true; 283 } 284 285 private TelephonyManager getTelephonyManager() { 286 return (TelephonyManager) getActivity().getSystemService(Context.TELEPHONY_SERVICE); 287 } 288 289 @Override 290 public Context getContext() { 291 return getActivity(); 292 } 293 294 @Override 295 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 296 wasEmptyBeforeTextChange = TextUtils.isEmpty(s); 297 } 298 299 @Override 300 public void onTextChanged(CharSequence input, int start, int before, int changeCount) { 301 if (wasEmptyBeforeTextChange != TextUtils.isEmpty(input)) { 302 final Activity activity = getActivity(); 303 if (activity != null) { 304 activity.invalidateOptionsMenu(); 305 updateMenuOverflowButton(wasEmptyBeforeTextChange); 306 } 307 } 308 309 // DTMF Tones do not need to be played here any longer - 310 // the DTMF dialer handles that functionality now. 311 } 312 313 @Override 314 public void afterTextChanged(Editable input) { 315 // When DTMF dialpad buttons are being pressed, we delay SpecialCharSequenceMgr sequence, 316 // since some of SpecialCharSequenceMgr's behavior is too abrupt for the "touch-down" 317 // behavior. 318 if (!digitsFilledByIntent 319 && SpecialCharSequenceMgr.handleChars(getActivity(), input.toString(), digits)) { 320 // A special sequence was entered, clear the digits 321 digits.getText().clear(); 322 } 323 324 if (isDigitsEmpty()) { 325 digitsFilledByIntent = false; 326 digits.setCursorVisible(false); 327 } 328 329 if (dialpadQueryListener != null) { 330 dialpadQueryListener.onDialpadQueryChanged(digits.getText().toString()); 331 } 332 333 updateDeleteButtonEnabledState(); 334 } 335 336 @Override 337 public void onCreate(Bundle state) { 338 Trace.beginSection(TAG + " onCreate"); 339 LogUtil.enterBlock("DialpadFragment.onCreate"); 340 super.onCreate(state); 341 342 firstLaunch = state == null; 343 344 prohibitedPhoneNumberRegexp = 345 getResources().getString(R.string.config_prohibited_phone_number_regexp); 346 347 if (state != null) { 348 digitsFilledByIntent = state.getBoolean(PREF_DIGITS_FILLED_BY_INTENT); 349 isDialpadSlideUp = state.getBoolean(PREF_IS_DIALPAD_SLIDE_OUT); 350 } 351 352 dialpadSlideInDuration = getResources().getInteger(R.integer.dialpad_slide_in_duration); 353 354 if (callStateReceiver == null) { 355 IntentFilter callStateIntentFilter = 356 new IntentFilter(TelephonyManager.ACTION_PHONE_STATE_CHANGED); 357 callStateReceiver = new CallStateReceiver(); 358 getActivity().registerReceiver(callStateReceiver, callStateIntentFilter); 359 } 360 361 initPhoneNumberFormattingTextWatcherExecutor = 362 DialerExecutorComponent.get(getContext()) 363 .dialerExecutorFactory() 364 .createUiTaskBuilder( 365 getFragmentManager(), 366 "DialpadFragment.initPhoneNumberFormattingTextWatcher", 367 new InitPhoneNumberFormattingTextWatcherWorker()) 368 .onSuccess(watcher -> dialpadView.getDigits().addTextChangedListener(watcher)) 369 .build(); 370 Trace.endSection(); 371 } 372 373 @Override 374 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 375 Trace.beginSection(TAG + " onCreateView"); 376 LogUtil.enterBlock("DialpadFragment.onCreateView"); 377 Trace.beginSection(TAG + " inflate view"); 378 View fragmentView = inflater.inflate(R.layout.dialpad_fragment, container, false); 379 Trace.endSection(); 380 Trace.beginSection(TAG + " buildLayer"); 381 fragmentView.buildLayer(); 382 Trace.endSection(); 383 384 Trace.beginSection(TAG + " setup views"); 385 386 dialpadView = fragmentView.findViewById(R.id.dialpad_view); 387 dialpadView.setCanDigitsBeEdited(true); 388 digits = dialpadView.getDigits(); 389 digits.setKeyListener(UnicodeDialerKeyListener.INSTANCE); 390 digits.setOnClickListener(this); 391 digits.setOnKeyListener(this); 392 digits.setOnLongClickListener(this); 393 digits.addTextChangedListener(this); 394 digits.setElegantTextHeight(false); 395 396 initPhoneNumberFormattingTextWatcherExecutor.executeSerial(getCurrentCountryIso()); 397 398 // Check for the presence of the keypad 399 View oneButton = fragmentView.findViewById(R.id.one); 400 if (oneButton != null) { 401 configureKeypadListeners(fragmentView); 402 } 403 404 delete = dialpadView.getDeleteButton(); 405 406 if (delete != null) { 407 delete.setOnClickListener(this); 408 delete.setOnLongClickListener(this); 409 } 410 411 fragmentView 412 .findViewById(R.id.spacer) 413 .setOnTouchListener( 414 (v, event) -> { 415 if (isDigitsEmpty()) { 416 if (getActivity() != null) { 417 LogUtil.i("DialpadFragment.onCreateView", "dialpad spacer touched"); 418 return FragmentUtils.getParentUnsafe(this, HostInterface.class) 419 .onDialpadSpacerTouchWithEmptyQuery(); 420 } 421 return true; 422 } 423 return false; 424 }); 425 426 digits.setCursorVisible(false); 427 428 // Set up the "dialpad chooser" UI; see showDialpadChooser(). 429 dialpadChooser = fragmentView.findViewById(R.id.dialpadChooser); 430 dialpadChooser.setOnItemClickListener(this); 431 432 floatingActionButton = fragmentView.findViewById(R.id.dialpad_floating_action_button); 433 floatingActionButton.setOnClickListener(this); 434 floatingActionButtonController = 435 new FloatingActionButtonController(getActivity(), floatingActionButton); 436 Trace.endSection(); 437 Trace.endSection(); 438 return fragmentView; 439 } 440 441 @Override 442 public void onAttach(Context context) { 443 super.onAttach(context); 444 isLayoutRtl = ViewUtil.isRtl(); 445 isLandscape = 446 getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; 447 } 448 449 private String getCurrentCountryIso() { 450 if (currentCountryIsoForTesting.isPresent()) { 451 return currentCountryIsoForTesting.get(); 452 } 453 454 return GeoUtil.getCurrentCountryIso(getActivity()); 455 } 456 457 @VisibleForTesting(otherwise = VisibleForTesting.NONE) 458 public static void setCurrentCountryIsoForTesting(String countryCode) { 459 currentCountryIsoForTesting = Optional.of(countryCode); 460 } 461 462 private boolean isLayoutReady() { 463 return digits != null; 464 } 465 466 public EditText getDigitsWidget() { 467 return digits; 468 } 469 470 /** @return true when {@link #digits} is actually filled by the Intent. */ 471 private boolean fillDigitsIfNecessary(Intent intent) { 472 // Only fills digits from an intent if it is a new intent. 473 // Otherwise falls back to the previously used number. 474 if (!firstLaunch && !startedFromNewIntent) { 475 return false; 476 } 477 478 final String action = intent.getAction(); 479 if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) { 480 Uri uri = intent.getData(); 481 if (uri != null) { 482 if (PhoneAccount.SCHEME_TEL.equals(uri.getScheme())) { 483 // Put the requested number into the input area 484 String data = uri.getSchemeSpecificPart(); 485 // Remember it is filled via Intent. 486 digitsFilledByIntent = true; 487 final String converted = 488 PhoneNumberUtils.convertKeypadLettersToDigits( 489 PhoneNumberUtils.replaceUnicodeDigits(data)); 490 setFormattedDigits(converted, null); 491 return true; 492 } else { 493 if (!PermissionsUtil.hasContactsReadPermissions(getActivity())) { 494 return false; 495 } 496 String type = intent.getType(); 497 if (People.CONTENT_ITEM_TYPE.equals(type) || Phones.CONTENT_ITEM_TYPE.equals(type)) { 498 // Query the phone number 499 Cursor c = 500 getActivity() 501 .getContentResolver() 502 .query( 503 intent.getData(), 504 new String[] {PhonesColumns.NUMBER, PhonesColumns.NUMBER_KEY}, 505 null, 506 null, 507 null); 508 if (c != null) { 509 try { 510 if (c.moveToFirst()) { 511 // Remember it is filled via Intent. 512 digitsFilledByIntent = true; 513 // Put the number into the input area 514 setFormattedDigits(c.getString(0), c.getString(1)); 515 return true; 516 } 517 } finally { 518 c.close(); 519 } 520 } 521 } 522 } 523 } 524 } 525 return false; 526 } 527 528 /** 529 * Checks the given Intent and changes dialpad's UI state. 530 * 531 * <p>There are three modes: 532 * 533 * <ul> 534 * <li>Empty Dialpad shown via "Add Call" in the in call ui 535 * <li>Dialpad (digits filled), shown by {@link Intent#ACTION_DIAL} with a number. 536 * <li>Return to call view, shown when a call is ongoing without {@link Intent#ACTION_DIAL} 537 * </ul> 538 * 539 * For example, if the user... 540 * 541 * <ul> 542 * <li>clicks a number in gmail, this method will show the dialpad filled with the number, 543 * regardless of whether a call is ongoing. 544 * <li>places a call, presses home and opens dialer, this method will show the return to call 545 * prompt to confirm what they want to do. 546 * </ul> 547 */ 548 private void configureScreenFromIntent(@NonNull Intent intent) { 549 LogUtil.i("DialpadFragment.configureScreenFromIntent", "action: %s", intent.getAction()); 550 if (!isLayoutReady()) { 551 // This happens typically when parent's Activity#onNewIntent() is called while 552 // Fragment#onCreateView() isn't called yet, and thus we cannot configure Views at 553 // this point. onViewCreate() should call this method after preparing layouts, so 554 // just ignore this call now. 555 LogUtil.i( 556 "DialpadFragment.configureScreenFromIntent", 557 "Screen configuration is requested before onCreateView() is called. Ignored"); 558 return; 559 } 560 561 // If "Add call" was selected, show the dialpad instead of the dialpad chooser prompt 562 if (isAddCallMode(intent)) { 563 LogUtil.i("DialpadFragment.configureScreenFromIntent", "Add call mode"); 564 showDialpadChooser(false); 565 setStartedFromNewIntent(true); 566 return; 567 } 568 569 // Don't show the chooser when called via onNewIntent() and phone number is present. 570 // i.e. User clicks a telephone link from gmail for example. 571 // In this case, we want to show the dialpad with the phone number. 572 boolean digitsFilled = fillDigitsIfNecessary(intent); 573 if (!(startedFromNewIntent && digitsFilled) && isPhoneInUse()) { 574 // If there's already an active call, bring up an intermediate UI to 575 // make the user confirm what they really want to do. 576 LogUtil.i("DialpadFragment.configureScreenFromIntent", "Dialpad chooser mode"); 577 showDialpadChooser(true); 578 setStartedFromNewIntent(false); 579 return; 580 } 581 582 LogUtil.i("DialpadFragment.configureScreenFromIntent", "Nothing to show"); 583 showDialpadChooser(false); 584 setStartedFromNewIntent(false); 585 } 586 587 public void setStartedFromNewIntent(boolean value) { 588 startedFromNewIntent = value; 589 } 590 591 public void clearCallRateInformation() { 592 setCallRateInformation(null, null); 593 } 594 595 public void setCallRateInformation(String countryName, String displayRate) { 596 dialpadView.setCallRateInformation(countryName, displayRate); 597 } 598 599 /** Sets formatted digits to digits field. */ 600 private void setFormattedDigits(String data, String normalizedNumber) { 601 final String formatted = getFormattedDigits(data, normalizedNumber, getCurrentCountryIso()); 602 if (!TextUtils.isEmpty(formatted)) { 603 Editable digits = this.digits.getText(); 604 digits.replace(0, digits.length(), formatted); 605 // for some reason this isn't getting called in the digits.replace call above.. 606 // but in any case, this will make sure the background drawable looks right 607 afterTextChanged(digits); 608 } 609 } 610 611 private void configureKeypadListeners(View fragmentView) { 612 final int[] buttonIds = 613 new int[] { 614 R.id.one, 615 R.id.two, 616 R.id.three, 617 R.id.four, 618 R.id.five, 619 R.id.six, 620 R.id.seven, 621 R.id.eight, 622 R.id.nine, 623 R.id.star, 624 R.id.zero, 625 R.id.pound 626 }; 627 628 DialpadKeyButton dialpadKey; 629 630 for (int buttonId : buttonIds) { 631 dialpadKey = fragmentView.findViewById(buttonId); 632 dialpadKey.setOnPressedListener(this); 633 } 634 635 // Long-pressing one button will initiate Voicemail. 636 final DialpadKeyButton one = fragmentView.findViewById(R.id.one); 637 one.setOnLongClickListener(this); 638 639 // Long-pressing zero button will enter '+' instead. 640 final DialpadKeyButton zero = fragmentView.findViewById(R.id.zero); 641 zero.setOnLongClickListener(this); 642 } 643 644 @Override 645 public void onStart() { 646 LogUtil.i("DialpadFragment.onStart", "first launch: %b", firstLaunch); 647 Trace.beginSection(TAG + " onStart"); 648 super.onStart(); 649 // if the mToneGenerator creation fails, just continue without it. It is 650 // a local audio signal, and is not as important as the dtmf tone itself. 651 final long start = System.currentTimeMillis(); 652 synchronized (toneGeneratorLock) { 653 if (toneGenerator == null) { 654 try { 655 toneGenerator = new ToneGenerator(DIAL_TONE_STREAM_TYPE, TONE_RELATIVE_VOLUME); 656 } catch (RuntimeException e) { 657 LogUtil.e( 658 "DialpadFragment.onStart", 659 "Exception caught while creating local tone generator: " + e); 660 toneGenerator = null; 661 } 662 } 663 } 664 final long total = System.currentTimeMillis() - start; 665 if (total > 50) { 666 LogUtil.i("DialpadFragment.onStart", "Time for ToneGenerator creation: " + total); 667 } 668 Trace.endSection(); 669 } 670 671 @Override 672 public void onResume() { 673 LogUtil.enterBlock("DialpadFragment.onResume"); 674 Trace.beginSection(TAG + " onResume"); 675 super.onResume(); 676 677 Resources res = getResources(); 678 int iconId = R.drawable.quantum_ic_call_vd_theme_24; 679 if (MotorolaUtils.isWifiCallingAvailable(getContext())) { 680 iconId = R.drawable.ic_wifi_calling; 681 } 682 floatingActionButtonController.changeIcon( 683 getContext(), iconId, res.getString(R.string.description_dial_button)); 684 685 dialpadQueryListener = FragmentUtils.getParentUnsafe(this, OnDialpadQueryChangedListener.class); 686 687 final StopWatch stopWatch = StopWatch.start("Dialpad.onResume"); 688 689 // Query the last dialed number. Do it first because hitting 690 // the DB is 'slow'. This call is asynchronous. 691 queryLastOutgoingCall(); 692 693 stopWatch.lap("qloc"); 694 695 final ContentResolver contentResolver = getActivity().getContentResolver(); 696 697 // retrieve the DTMF tone play back setting. 698 dTMFToneEnabled = 699 Settings.System.getInt(contentResolver, Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1; 700 701 stopWatch.lap("dtwd"); 702 703 stopWatch.lap("hptc"); 704 705 pressedDialpadKeys.clear(); 706 707 configureScreenFromIntent(getActivity().getIntent()); 708 709 stopWatch.lap("fdin"); 710 711 if (!isPhoneInUse()) { 712 LogUtil.i("DialpadFragment.onResume", "phone not in use"); 713 // A sanity-check: the "dialpad chooser" UI should not be visible if the phone is idle. 714 showDialpadChooser(false); 715 } 716 717 stopWatch.lap("hnt"); 718 719 updateDeleteButtonEnabledState(); 720 721 stopWatch.lap("bes"); 722 723 stopWatch.stopAndLog(TAG, 50); 724 725 // Populate the overflow menu in onResume instead of onCreate, so that if the SMS activity 726 // is disabled while Dialer is paused, the "Send a text message" option can be correctly 727 // removed when resumed. 728 overflowMenuButton = dialpadView.getOverflowMenuButton(); 729 overflowPopupMenu = buildOptionsMenu(overflowMenuButton); 730 overflowMenuButton.setOnTouchListener(overflowPopupMenu.getDragToOpenListener()); 731 overflowMenuButton.setOnClickListener(this); 732 overflowMenuButton.setVisibility(isDigitsEmpty() ? View.INVISIBLE : View.VISIBLE); 733 734 if (firstLaunch) { 735 // The onHiddenChanged callback does not get called the first time the fragment is 736 // attached, so call it ourselves here. 737 onHiddenChanged(false); 738 } 739 740 firstLaunch = false; 741 Trace.endSection(); 742 } 743 744 @Override 745 public void onPause() { 746 super.onPause(); 747 748 // Make sure we don't leave this activity with a tone still playing. 749 stopTone(); 750 pressedDialpadKeys.clear(); 751 752 // TODO: I wonder if we should not check if the AsyncTask that 753 // lookup the last dialed number has completed. 754 lastNumberDialed = EMPTY_NUMBER; // Since we are going to query again, free stale number. 755 756 SpecialCharSequenceMgr.cleanup(); 757 overflowPopupMenu.dismiss(); 758 } 759 760 @Override 761 public void onStop() { 762 LogUtil.enterBlock("DialpadFragment.onStop"); 763 super.onStop(); 764 765 synchronized (toneGeneratorLock) { 766 if (toneGenerator != null) { 767 toneGenerator.release(); 768 toneGenerator = null; 769 } 770 } 771 772 if (clearDigitsOnStop) { 773 clearDigitsOnStop = false; 774 clearDialpad(); 775 } 776 } 777 778 @Override 779 public void onSaveInstanceState(Bundle outState) { 780 super.onSaveInstanceState(outState); 781 outState.putBoolean(PREF_DIGITS_FILLED_BY_INTENT, digitsFilledByIntent); 782 outState.putBoolean(PREF_IS_DIALPAD_SLIDE_OUT, isDialpadSlideUp); 783 } 784 785 @Override 786 public void onDestroy() { 787 super.onDestroy(); 788 if (pseudoEmergencyAnimator != null) { 789 pseudoEmergencyAnimator.destroy(); 790 pseudoEmergencyAnimator = null; 791 } 792 getActivity().unregisterReceiver(callStateReceiver); 793 } 794 795 private void keyPressed(int keyCode) { 796 if (getView() == null || getView().getTranslationY() != 0) { 797 return; 798 } 799 switch (keyCode) { 800 case KeyEvent.KEYCODE_1: 801 playTone(ToneGenerator.TONE_DTMF_1, TONE_LENGTH_INFINITE); 802 break; 803 case KeyEvent.KEYCODE_2: 804 playTone(ToneGenerator.TONE_DTMF_2, TONE_LENGTH_INFINITE); 805 break; 806 case KeyEvent.KEYCODE_3: 807 playTone(ToneGenerator.TONE_DTMF_3, TONE_LENGTH_INFINITE); 808 break; 809 case KeyEvent.KEYCODE_4: 810 playTone(ToneGenerator.TONE_DTMF_4, TONE_LENGTH_INFINITE); 811 break; 812 case KeyEvent.KEYCODE_5: 813 playTone(ToneGenerator.TONE_DTMF_5, TONE_LENGTH_INFINITE); 814 break; 815 case KeyEvent.KEYCODE_6: 816 playTone(ToneGenerator.TONE_DTMF_6, TONE_LENGTH_INFINITE); 817 break; 818 case KeyEvent.KEYCODE_7: 819 playTone(ToneGenerator.TONE_DTMF_7, TONE_LENGTH_INFINITE); 820 break; 821 case KeyEvent.KEYCODE_8: 822 playTone(ToneGenerator.TONE_DTMF_8, TONE_LENGTH_INFINITE); 823 break; 824 case KeyEvent.KEYCODE_9: 825 playTone(ToneGenerator.TONE_DTMF_9, TONE_LENGTH_INFINITE); 826 break; 827 case KeyEvent.KEYCODE_0: 828 playTone(ToneGenerator.TONE_DTMF_0, TONE_LENGTH_INFINITE); 829 break; 830 case KeyEvent.KEYCODE_POUND: 831 playTone(ToneGenerator.TONE_DTMF_P, TONE_LENGTH_INFINITE); 832 break; 833 case KeyEvent.KEYCODE_STAR: 834 playTone(ToneGenerator.TONE_DTMF_S, TONE_LENGTH_INFINITE); 835 break; 836 default: 837 break; 838 } 839 840 getView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); 841 KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode); 842 digits.onKeyDown(keyCode, event); 843 844 // If the cursor is at the end of the text we hide it. 845 final int length = digits.length(); 846 if (length == digits.getSelectionStart() && length == digits.getSelectionEnd()) { 847 digits.setCursorVisible(false); 848 } 849 } 850 851 @Override 852 public boolean onKey(View view, int keyCode, KeyEvent event) { 853 if (view.getId() == R.id.digits) { 854 if (keyCode == KeyEvent.KEYCODE_ENTER) { 855 handleDialButtonPressed(); 856 return true; 857 } 858 } 859 return false; 860 } 861 862 /** 863 * When a key is pressed, we start playing DTMF tone, do vibration, and enter the digit 864 * immediately. When a key is released, we stop the tone. Note that the "key press" event will be 865 * delivered by the system with certain amount of delay, it won't be synced with user's actual 866 * "touch-down" behavior. 867 */ 868 @Override 869 public void onPressed(View view, boolean pressed) { 870 if (pressed) { 871 int resId = view.getId(); 872 if (resId == R.id.one) { 873 keyPressed(KeyEvent.KEYCODE_1); 874 } else if (resId == R.id.two) { 875 keyPressed(KeyEvent.KEYCODE_2); 876 } else if (resId == R.id.three) { 877 keyPressed(KeyEvent.KEYCODE_3); 878 } else if (resId == R.id.four) { 879 keyPressed(KeyEvent.KEYCODE_4); 880 } else if (resId == R.id.five) { 881 keyPressed(KeyEvent.KEYCODE_5); 882 } else if (resId == R.id.six) { 883 keyPressed(KeyEvent.KEYCODE_6); 884 } else if (resId == R.id.seven) { 885 keyPressed(KeyEvent.KEYCODE_7); 886 } else if (resId == R.id.eight) { 887 keyPressed(KeyEvent.KEYCODE_8); 888 } else if (resId == R.id.nine) { 889 keyPressed(KeyEvent.KEYCODE_9); 890 } else if (resId == R.id.zero) { 891 keyPressed(KeyEvent.KEYCODE_0); 892 } else if (resId == R.id.pound) { 893 keyPressed(KeyEvent.KEYCODE_POUND); 894 } else if (resId == R.id.star) { 895 keyPressed(KeyEvent.KEYCODE_STAR); 896 } else { 897 LogUtil.e( 898 "DialpadFragment.onPressed", "Unexpected onTouch(ACTION_DOWN) event from: " + view); 899 } 900 pressedDialpadKeys.add(view); 901 } else { 902 pressedDialpadKeys.remove(view); 903 if (pressedDialpadKeys.isEmpty()) { 904 stopTone(); 905 } 906 } 907 } 908 909 /** 910 * Called by the containing Activity to tell this Fragment to build an overflow options menu for 911 * display by the container when appropriate. 912 * 913 * @param invoker the View that invoked the options menu, to act as an anchor location. 914 */ 915 private PopupMenu buildOptionsMenu(View invoker) { 916 final PopupMenu popupMenu = 917 new PopupMenu(getActivity(), invoker) { 918 @Override 919 public void show() { 920 final Menu menu = getMenu(); 921 922 boolean enable = !isDigitsEmpty(); 923 for (int i = 0; i < menu.size(); i++) { 924 MenuItem item = menu.getItem(i); 925 item.setEnabled(enable); 926 if (item.getItemId() == R.id.menu_call_with_note) { 927 item.setVisible(CallUtil.isCallWithSubjectSupported(getContext())); 928 } 929 } 930 super.show(); 931 } 932 }; 933 popupMenu.inflate(R.menu.dialpad_options); 934 popupMenu.setOnMenuItemClickListener(this); 935 return popupMenu; 936 } 937 938 @Override 939 public void onClick(View view) { 940 int resId = view.getId(); 941 if (resId == R.id.dialpad_floating_action_button) { 942 view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); 943 handleDialButtonPressed(); 944 } else if (resId == R.id.deleteButton) { 945 keyPressed(KeyEvent.KEYCODE_DEL); 946 } else if (resId == R.id.digits) { 947 if (!isDigitsEmpty()) { 948 digits.setCursorVisible(true); 949 } 950 } else if (resId == R.id.dialpad_overflow) { 951 overflowPopupMenu.show(); 952 } else { 953 LogUtil.w("DialpadFragment.onClick", "Unexpected event from: " + view); 954 } 955 } 956 957 @Override 958 public boolean onLongClick(View view) { 959 final Editable digits = this.digits.getText(); 960 final int id = view.getId(); 961 if (id == R.id.deleteButton) { 962 digits.clear(); 963 return true; 964 } else if (id == R.id.one) { 965 if (isDigitsEmpty() || TextUtils.equals(this.digits.getText(), "1")) { 966 // We'll try to initiate voicemail and thus we want to remove irrelevant string. 967 removePreviousDigitIfPossible('1'); 968 969 List<PhoneAccountHandle> subscriptionAccountHandles = 970 TelecomUtil.getSubscriptionPhoneAccounts(getActivity()); 971 boolean hasUserSelectedDefault = 972 subscriptionAccountHandles.contains( 973 TelecomUtil.getDefaultOutgoingPhoneAccount( 974 getActivity(), PhoneAccount.SCHEME_VOICEMAIL)); 975 boolean needsAccountDisambiguation = 976 subscriptionAccountHandles.size() > 1 && !hasUserSelectedDefault; 977 978 if (needsAccountDisambiguation || isVoicemailAvailable()) { 979 // On a multi-SIM phone, if the user has not selected a default 980 // subscription, initiate a call to voicemail so they can select an account 981 // from the "Call with" dialog. 982 callVoicemail(); 983 } else if (getActivity() != null) { 984 // Voicemail is unavailable maybe because Airplane mode is turned on. 985 // Check the current status and show the most appropriate error message. 986 final boolean isAirplaneModeOn = 987 Settings.System.getInt( 988 getActivity().getContentResolver(), Settings.System.AIRPLANE_MODE_ON, 0) 989 != 0; 990 if (isAirplaneModeOn) { 991 DialogFragment dialogFragment = 992 ErrorDialogFragment.newInstance(R.string.dialog_voicemail_airplane_mode_message); 993 dialogFragment.show(getFragmentManager(), "voicemail_request_during_airplane_mode"); 994 } else { 995 DialogFragment dialogFragment = 996 ErrorDialogFragment.newInstance(R.string.dialog_voicemail_not_ready_message); 997 dialogFragment.show(getFragmentManager(), "voicemail_not_ready"); 998 } 999 } 1000 return true; 1001 } 1002 return false; 1003 } else if (id == R.id.zero) { 1004 if (pressedDialpadKeys.contains(view)) { 1005 // If the zero key is currently pressed, then the long press occurred by touch 1006 // (and not via other means like certain accessibility input methods). 1007 // Remove the '0' that was input when the key was first pressed. 1008 removePreviousDigitIfPossible('0'); 1009 } 1010 keyPressed(KeyEvent.KEYCODE_PLUS); 1011 stopTone(); 1012 pressedDialpadKeys.remove(view); 1013 return true; 1014 } else if (id == R.id.digits) { 1015 this.digits.setCursorVisible(true); 1016 return false; 1017 } 1018 return false; 1019 } 1020 1021 /** 1022 * Remove the digit just before the current position of the cursor, iff the following conditions 1023 * are true: 1) The cursor is not positioned at index 0. 2) The digit before the current cursor 1024 * position matches the current digit. 1025 * 1026 * @param digit to remove from the digits view. 1027 */ 1028 private void removePreviousDigitIfPossible(char digit) { 1029 final int currentPosition = digits.getSelectionStart(); 1030 if (currentPosition > 0 && digit == digits.getText().charAt(currentPosition - 1)) { 1031 digits.setSelection(currentPosition); 1032 digits.getText().delete(currentPosition - 1, currentPosition); 1033 } 1034 } 1035 1036 public void callVoicemail() { 1037 PreCall.start( 1038 getContext(), CallIntentBuilder.forVoicemail(null, CallInitiationType.Type.DIALPAD)); 1039 hideAndClearDialpad(); 1040 } 1041 1042 private void hideAndClearDialpad() { 1043 LogUtil.enterBlock("DialpadFragment.hideAndClearDialpad"); 1044 FragmentUtils.getParentUnsafe(this, DialpadListener.class).onCallPlacedFromDialpad(); 1045 } 1046 1047 /** 1048 * In most cases, when the dial button is pressed, there is a number in digits area. Pack it in 1049 * the intent, start the outgoing call broadcast as a separate task and finish this activity. 1050 * 1051 * <p>When there is no digit and the phone is CDMA and off hook, we're sending a blank flash for 1052 * CDMA. CDMA networks use Flash messages when special processing needs to be done, mainly for 1053 * 3-way or call waiting scenarios. Presumably, here we're in a special 3-way scenario where the 1054 * network needs a blank flash before being able to add the new participant. (This is not the case 1055 * with all 3-way calls, just certain CDMA infrastructures.) 1056 * 1057 * <p>Otherwise, there is no digit, display the last dialed number. Don't finish since the user 1058 * may want to edit it. The user needs to press the dial button again, to dial it (general case 1059 * described above). 1060 */ 1061 private void handleDialButtonPressed() { 1062 if (isDigitsEmpty()) { // No number entered. 1063 // No real call made, so treat it as a click 1064 PerformanceReport.recordClick(UiAction.Type.PRESS_CALL_BUTTON_WITHOUT_CALLING); 1065 handleDialButtonClickWithEmptyDigits(); 1066 } else { 1067 final String number = digits.getText().toString(); 1068 1069 // "persist.radio.otaspdial" is a temporary hack needed for one carrier's automated 1070 // test equipment. 1071 // TODO: clean it up. 1072 if (number != null 1073 && !TextUtils.isEmpty(prohibitedPhoneNumberRegexp) 1074 && number.matches(prohibitedPhoneNumberRegexp)) { 1075 PerformanceReport.recordClick(UiAction.Type.PRESS_CALL_BUTTON_WITHOUT_CALLING); 1076 LogUtil.i( 1077 "DialpadFragment.handleDialButtonPressed", 1078 "The phone number is prohibited explicitly by a rule."); 1079 if (getActivity() != null) { 1080 DialogFragment dialogFragment = 1081 ErrorDialogFragment.newInstance(R.string.dialog_phone_call_prohibited_message); 1082 dialogFragment.show(getFragmentManager(), "phone_prohibited_dialog"); 1083 } 1084 1085 // Clear the digits just in case. 1086 clearDialpad(); 1087 } else { 1088 PreCall.start(getContext(), new CallIntentBuilder(number, CallInitiationType.Type.DIALPAD)); 1089 hideAndClearDialpad(); 1090 } 1091 } 1092 } 1093 1094 public void clearDialpad() { 1095 if (digits != null) { 1096 digits.getText().clear(); 1097 } 1098 } 1099 1100 private void handleDialButtonClickWithEmptyDigits() { 1101 if (phoneIsCdma() && isPhoneInUse()) { 1102 // TODO: Move this logic into services/Telephony 1103 // 1104 // This is really CDMA specific. On GSM is it possible 1105 // to be off hook and wanted to add a 3rd party using 1106 // the redial feature. 1107 startActivity(newFlashIntent()); 1108 } else { 1109 if (!TextUtils.isEmpty(lastNumberDialed)) { 1110 // Dialpad will be filled with last called number, 1111 // but we don't want to record it as user action 1112 PerformanceReport.setIgnoreActionOnce(UiAction.Type.TEXT_CHANGE_WITH_INPUT); 1113 1114 // Recall the last number dialed. 1115 digits.setText(lastNumberDialed); 1116 1117 // ...and move the cursor to the end of the digits string, 1118 // so you'll be able to delete digits using the Delete 1119 // button (just as if you had typed the number manually.) 1120 // 1121 // Note we use mDigits.getText().length() here, not 1122 // mLastNumberDialed.length(), since the EditText widget now 1123 // contains a *formatted* version of mLastNumberDialed (due to 1124 // mTextWatcher) and its length may have changed. 1125 digits.setSelection(digits.getText().length()); 1126 } else { 1127 // There's no "last number dialed" or the 1128 // background query is still running. There's 1129 // nothing useful for the Dial button to do in 1130 // this case. Note: with a soft dial button, this 1131 // can never happens since the dial button is 1132 // disabled under these conditons. 1133 playTone(ToneGenerator.TONE_PROP_NACK); 1134 } 1135 } 1136 } 1137 1138 /** Plays the specified tone for TONE_LENGTH_MS milliseconds. */ 1139 private void playTone(int tone) { 1140 playTone(tone, TONE_LENGTH_MS); 1141 } 1142 1143 /** 1144 * Play the specified tone for the specified milliseconds 1145 * 1146 * <p>The tone is played locally, using the audio stream for phone calls. Tones are played only if 1147 * the "Audible touch tones" user preference is checked, and are NOT played if the device is in 1148 * silent mode. 1149 * 1150 * <p>The tone length can be -1, meaning "keep playing the tone." If the caller does so, it should 1151 * call stopTone() afterward. 1152 * 1153 * @param tone a tone code from {@link ToneGenerator} 1154 * @param durationMs tone length. 1155 */ 1156 private void playTone(int tone, int durationMs) { 1157 // if local tone playback is disabled, just return. 1158 if (!dTMFToneEnabled) { 1159 return; 1160 } 1161 1162 // Also do nothing if the phone is in silent mode. 1163 // We need to re-check the ringer mode for *every* playTone() 1164 // call, rather than keeping a local flag that's updated in 1165 // onResume(), since it's possible to toggle silent mode without 1166 // leaving the current activity (via the ENDCALL-longpress menu.) 1167 AudioManager audioManager = 1168 (AudioManager) getActivity().getSystemService(Context.AUDIO_SERVICE); 1169 int ringerMode = audioManager.getRingerMode(); 1170 if ((ringerMode == AudioManager.RINGER_MODE_SILENT) 1171 || (ringerMode == AudioManager.RINGER_MODE_VIBRATE)) { 1172 return; 1173 } 1174 1175 synchronized (toneGeneratorLock) { 1176 if (toneGenerator == null) { 1177 LogUtil.w("DialpadFragment.playTone", "mToneGenerator == null, tone: " + tone); 1178 return; 1179 } 1180 1181 // Start the new tone (will stop any playing tone) 1182 toneGenerator.startTone(tone, durationMs); 1183 } 1184 } 1185 1186 /** Stop the tone if it is played. */ 1187 private void stopTone() { 1188 // if local tone playback is disabled, just return. 1189 if (!dTMFToneEnabled) { 1190 return; 1191 } 1192 synchronized (toneGeneratorLock) { 1193 if (toneGenerator == null) { 1194 LogUtil.w("DialpadFragment.stopTone", "mToneGenerator == null"); 1195 return; 1196 } 1197 toneGenerator.stopTone(); 1198 } 1199 } 1200 1201 /** 1202 * Brings up the "dialpad chooser" UI in place of the usual Dialer elements (the textfield/button 1203 * and the dialpad underneath). 1204 * 1205 * <p>We show this UI if the user brings up the Dialer while a call is already in progress, since 1206 * there's a good chance we got here accidentally (and the user really wanted the in-call dialpad 1207 * instead). So in this situation we display an intermediate UI that lets the user explicitly 1208 * choose between the in-call dialpad ("Use touch tone keypad") and the regular Dialer ("Add 1209 * call"). (Or, the option "Return to call in progress" just goes back to the in-call UI with no 1210 * dialpad at all.) 1211 * 1212 * @param enabled If true, show the "dialpad chooser" instead of the regular Dialer UI 1213 */ 1214 private void showDialpadChooser(boolean enabled) { 1215 if (getActivity() == null) { 1216 return; 1217 } 1218 // Check if onCreateView() is already called by checking one of View objects. 1219 if (!isLayoutReady()) { 1220 return; 1221 } 1222 1223 if (enabled) { 1224 LogUtil.i("DialpadFragment.showDialpadChooser", "Showing dialpad chooser!"); 1225 if (dialpadView != null) { 1226 dialpadView.setVisibility(View.GONE); 1227 } 1228 1229 if (overflowPopupMenu != null) { 1230 overflowPopupMenu.dismiss(); 1231 } 1232 1233 floatingActionButtonController.scaleOut(); 1234 dialpadChooser.setVisibility(View.VISIBLE); 1235 1236 // Instantiate the DialpadChooserAdapter and hook it up to the 1237 // ListView. We do this only once. 1238 if (dialpadChooserAdapter == null) { 1239 dialpadChooserAdapter = new DialpadChooserAdapter(getActivity()); 1240 } 1241 dialpadChooser.setAdapter(dialpadChooserAdapter); 1242 } else { 1243 LogUtil.i("DialpadFragment.showDialpadChooser", "Displaying normal Dialer UI."); 1244 if (dialpadView != null) { 1245 LogUtil.i("DialpadFragment.showDialpadChooser", "mDialpadView not null"); 1246 dialpadView.setVisibility(View.VISIBLE); 1247 floatingActionButtonController.scaleIn(); 1248 } else { 1249 LogUtil.i("DialpadFragment.showDialpadChooser", "mDialpadView null"); 1250 digits.setVisibility(View.VISIBLE); 1251 } 1252 1253 // mFloatingActionButtonController must also be 'scaled in', in order to be visible after 1254 // 'scaleOut()' hidden method. 1255 if (!floatingActionButtonController.isVisible()) { 1256 // Just call 'scaleIn()' method if the mFloatingActionButtonController was not already 1257 // previously visible. 1258 floatingActionButtonController.scaleIn(); 1259 } 1260 dialpadChooser.setVisibility(View.GONE); 1261 } 1262 } 1263 1264 /** @return true if we're currently showing the "dialpad chooser" UI. */ 1265 private boolean isDialpadChooserVisible() { 1266 return dialpadChooser.getVisibility() == View.VISIBLE; 1267 } 1268 1269 /** Handle clicks from the dialpad chooser. */ 1270 @Override 1271 public void onItemClick(AdapterView<?> parent, View v, int position, long id) { 1272 DialpadChooserAdapter.ChoiceItem item = 1273 (DialpadChooserAdapter.ChoiceItem) parent.getItemAtPosition(position); 1274 int itemId = item.id; 1275 if (itemId == DialpadChooserAdapter.DIALPAD_CHOICE_USE_DTMF_DIALPAD) { 1276 // Fire off an intent to go back to the in-call UI 1277 // with the dialpad visible. 1278 returnToInCallScreen(true); 1279 } else if (itemId == DialpadChooserAdapter.DIALPAD_CHOICE_RETURN_TO_CALL) { 1280 // Fire off an intent to go back to the in-call UI 1281 // (with the dialpad hidden). 1282 returnToInCallScreen(false); 1283 } else if (itemId == DialpadChooserAdapter.DIALPAD_CHOICE_ADD_NEW_CALL) { 1284 // Ok, guess the user really did want to be here (in the 1285 // regular Dialer) after all. Bring back the normal Dialer UI. 1286 showDialpadChooser(false); 1287 } else { 1288 LogUtil.w("DialpadFragment.onItemClick", "Unexpected itemId: " + itemId); 1289 } 1290 } 1291 1292 /** 1293 * Returns to the in-call UI (where there's presumably a call in progress) in response to the user 1294 * selecting "use touch tone keypad" or "return to call" from the dialpad chooser. 1295 */ 1296 private void returnToInCallScreen(boolean showDialpad) { 1297 TelecomUtil.showInCallScreen(getActivity(), showDialpad); 1298 1299 // Finally, finish() ourselves so that we don't stay on the 1300 // activity stack. 1301 // Note that we do this whether or not the showCallScreenWithDialpad() 1302 // call above had any effect or not! (That call is a no-op if the 1303 // phone is idle, which can happen if the current call ends while 1304 // the dialpad chooser is up. In this case we can't show the 1305 // InCallScreen, and there's no point staying here in the Dialer, 1306 // so we just take the user back where he came from...) 1307 getActivity().finish(); 1308 } 1309 1310 /** 1311 * @return true if the phone is "in use", meaning that at least one line is active (ie. off hook 1312 * or ringing or dialing, or on hold). 1313 */ 1314 private boolean isPhoneInUse() { 1315 return getContext() != null 1316 && TelecomUtil.isInManagedCall(getContext()) 1317 && FragmentUtils.getParentUnsafe(this, HostInterface.class).shouldShowDialpadChooser(); 1318 } 1319 1320 /** @return true if the phone is a CDMA phone type */ 1321 private boolean phoneIsCdma() { 1322 return getTelephonyManager().getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA; 1323 } 1324 1325 @Override 1326 public boolean onMenuItemClick(MenuItem item) { 1327 int resId = item.getItemId(); 1328 if (resId == R.id.menu_2s_pause) { 1329 updateDialString(PAUSE); 1330 return true; 1331 } else if (resId == R.id.menu_add_wait) { 1332 updateDialString(WAIT); 1333 return true; 1334 } else if (resId == R.id.menu_call_with_note) { 1335 CallSubjectDialog.start(getActivity(), digits.getText().toString()); 1336 hideAndClearDialpad(); 1337 return true; 1338 } else { 1339 return false; 1340 } 1341 } 1342 1343 /** 1344 * Updates the dial string (mDigits) after inserting a Pause character (,) or Wait character (;). 1345 */ 1346 private void updateDialString(char newDigit) { 1347 if (newDigit != WAIT && newDigit != PAUSE) { 1348 throw new IllegalArgumentException("Not expected for anything other than PAUSE & WAIT"); 1349 } 1350 1351 int selectionStart; 1352 int selectionEnd; 1353 1354 // SpannableStringBuilder editable_text = new SpannableStringBuilder(mDigits.getText()); 1355 int anchor = this.digits.getSelectionStart(); 1356 int point = this.digits.getSelectionEnd(); 1357 1358 selectionStart = Math.min(anchor, point); 1359 selectionEnd = Math.max(anchor, point); 1360 1361 if (selectionStart == -1) { 1362 selectionStart = selectionEnd = this.digits.length(); 1363 } 1364 1365 Editable digits = this.digits.getText(); 1366 1367 if (canAddDigit(digits, selectionStart, selectionEnd, newDigit)) { 1368 digits.replace(selectionStart, selectionEnd, Character.toString(newDigit)); 1369 1370 if (selectionStart != selectionEnd) { 1371 // Unselect: back to a regular cursor, just pass the character inserted. 1372 this.digits.setSelection(selectionStart + 1); 1373 } 1374 } 1375 } 1376 1377 /** Update the enabledness of the "Dial" and "Backspace" buttons if applicable. */ 1378 private void updateDeleteButtonEnabledState() { 1379 if (getActivity() == null) { 1380 return; 1381 } 1382 final boolean digitsNotEmpty = !isDigitsEmpty(); 1383 delete.setEnabled(digitsNotEmpty); 1384 } 1385 1386 /** 1387 * Handle transitions for the menu button depending on the state of the digits edit text. 1388 * Transition out when going from digits to no digits and transition in when the first digit is 1389 * pressed. 1390 * 1391 * @param transitionIn True if transitioning in, False if transitioning out 1392 */ 1393 private void updateMenuOverflowButton(boolean transitionIn) { 1394 overflowMenuButton = dialpadView.getOverflowMenuButton(); 1395 if (transitionIn) { 1396 AnimUtils.fadeIn(overflowMenuButton, AnimUtils.DEFAULT_DURATION); 1397 } else { 1398 AnimUtils.fadeOut(overflowMenuButton, AnimUtils.DEFAULT_DURATION); 1399 } 1400 } 1401 1402 /** 1403 * Check if voicemail is enabled/accessible. 1404 * 1405 * @return true if voicemail is enabled and accessible. Note that this can be false "temporarily" 1406 * after the app boot. 1407 */ 1408 private boolean isVoicemailAvailable() { 1409 try { 1410 PhoneAccountHandle defaultUserSelectedAccount = 1411 TelecomUtil.getDefaultOutgoingPhoneAccount(getActivity(), PhoneAccount.SCHEME_VOICEMAIL); 1412 if (defaultUserSelectedAccount == null) { 1413 // In a single-SIM phone, there is no default outgoing phone account selected by 1414 // the user, so just call TelephonyManager#getVoicemailNumber directly. 1415 return !TextUtils.isEmpty(getTelephonyManager().getVoiceMailNumber()); 1416 } else { 1417 return !TextUtils.isEmpty( 1418 TelecomUtil.getVoicemailNumber(getActivity(), defaultUserSelectedAccount)); 1419 } 1420 } catch (SecurityException se) { 1421 // Possibly no READ_PHONE_STATE privilege. 1422 LogUtil.w( 1423 "DialpadFragment.isVoicemailAvailable", 1424 "SecurityException is thrown. Maybe privilege isn't sufficient."); 1425 } 1426 return false; 1427 } 1428 1429 /** @return true if the widget with the phone number digits is empty. */ 1430 private boolean isDigitsEmpty() { 1431 return digits.length() == 0; 1432 } 1433 1434 /** 1435 * Starts the asyn query to get the last dialed/outgoing number. When the background query 1436 * finishes, mLastNumberDialed is set to the last dialed number or an empty string if none exists 1437 * yet. 1438 */ 1439 private void queryLastOutgoingCall() { 1440 lastNumberDialed = EMPTY_NUMBER; 1441 if (!PermissionsUtil.hasCallLogReadPermissions(getContext())) { 1442 return; 1443 } 1444 FragmentUtils.getParentUnsafe(this, DialpadListener.class) 1445 .getLastOutgoingCall( 1446 number -> { 1447 // TODO: Filter out emergency numbers if the carrier does not want redial for these. 1448 1449 // If the fragment has already been detached since the last time we called 1450 // queryLastOutgoingCall in onResume there is no point doing anything here. 1451 if (getActivity() == null) { 1452 return; 1453 } 1454 lastNumberDialed = number; 1455 updateDeleteButtonEnabledState(); 1456 }); 1457 } 1458 1459 private Intent newFlashIntent() { 1460 Intent intent = new CallIntentBuilder(EMPTY_NUMBER, CallInitiationType.Type.DIALPAD).build(); 1461 intent.putExtra(EXTRA_SEND_EMPTY_FLASH, true); 1462 return intent; 1463 } 1464 1465 @Override 1466 public void onHiddenChanged(boolean hidden) { 1467 super.onHiddenChanged(hidden); 1468 if (getActivity() == null || getView() == null) { 1469 return; 1470 } 1471 if (!hidden && !isDialpadChooserVisible()) { 1472 if (animate) { 1473 dialpadView.animateShow(); 1474 } 1475 ThreadUtil.getUiThreadHandler() 1476 .postDelayed( 1477 () -> { 1478 if (!isDialpadChooserVisible()) { 1479 floatingActionButtonController.scaleIn(); 1480 } 1481 }, 1482 animate ? dialpadSlideInDuration : 0); 1483 FragmentUtils.getParentUnsafe(this, DialpadListener.class).onDialpadShown(); 1484 digits.requestFocus(); 1485 } else if (hidden) { 1486 floatingActionButtonController.scaleOut(); 1487 } 1488 } 1489 1490 public boolean getAnimate() { 1491 return animate; 1492 } 1493 1494 public void setAnimate(boolean value) { 1495 animate = value; 1496 } 1497 1498 public void setYFraction(float yFraction) { 1499 ((DialpadSlidingRelativeLayout) getView()).setYFraction(yFraction); 1500 } 1501 1502 public int getDialpadHeight() { 1503 if (dialpadView == null) { 1504 return 0; 1505 } 1506 return dialpadView.getHeight(); 1507 } 1508 1509 public void process_quote_emergency_unquote(String query) { 1510 if (PseudoEmergencyAnimator.PSEUDO_EMERGENCY_NUMBER.equals(query)) { 1511 if (pseudoEmergencyAnimator == null) { 1512 pseudoEmergencyAnimator = 1513 new PseudoEmergencyAnimator( 1514 new PseudoEmergencyAnimator.ViewProvider() { 1515 @Override 1516 public View getFab() { 1517 return floatingActionButton; 1518 } 1519 1520 @Override 1521 public Context getContext() { 1522 return DialpadFragment.this.getContext(); 1523 } 1524 }); 1525 } 1526 pseudoEmergencyAnimator.start(); 1527 } else { 1528 if (pseudoEmergencyAnimator != null) { 1529 pseudoEmergencyAnimator.end(); 1530 } 1531 } 1532 } 1533 1534 /** Animate the dialpad down off the screen. */ 1535 public void slideDown(boolean animate, AnimationListener listener) { 1536 Assert.checkArgument(isDialpadSlideUp); 1537 isDialpadSlideUp = false; 1538 int animation; 1539 if (isLandscape) { 1540 animation = isLayoutRtl ? R.anim.dialpad_slide_out_left : R.anim.dialpad_slide_out_right; 1541 } else { 1542 animation = R.anim.dialpad_slide_out_bottom; 1543 } 1544 Animation slideDown = AnimationUtils.loadAnimation(getContext(), animation); 1545 slideDown.setInterpolator(AnimUtils.EASE_OUT); 1546 slideDown.setAnimationListener(listener); 1547 slideDown.setDuration(animate ? dialpadSlideInDuration : 0); 1548 getView().startAnimation(slideDown); 1549 } 1550 1551 /** Animate the dialpad up onto the screen. */ 1552 public void slideUp(boolean animate) { 1553 Assert.checkArgument(!isDialpadSlideUp); 1554 isDialpadSlideUp = true; 1555 int animation; 1556 if (isLandscape) { 1557 animation = isLayoutRtl ? R.anim.dialpad_slide_in_left : R.anim.dialpad_slide_in_right; 1558 } else { 1559 animation = R.anim.dialpad_slide_in_bottom; 1560 } 1561 Animation slideUp = AnimationUtils.loadAnimation(getContext(), animation); 1562 slideUp.setInterpolator(AnimUtils.EASE_IN); 1563 slideUp.setDuration(animate ? dialpadSlideInDuration : 0); 1564 getView().startAnimation(slideUp); 1565 } 1566 1567 public boolean isDialpadSlideUp() { 1568 return isDialpadSlideUp; 1569 } 1570 1571 /** Returns the text in the dialpad */ 1572 public String getQuery() { 1573 return digits.getText().toString(); 1574 } 1575 1576 public interface OnDialpadQueryChangedListener { 1577 1578 void onDialpadQueryChanged(String query); 1579 } 1580 1581 public interface HostInterface { 1582 1583 /** 1584 * Notifies the parent activity that the space above the dialpad has been tapped with no query 1585 * in the dialpad present. In most situations this will cause the dialpad to be dismissed, 1586 * unless there happens to be content showing. 1587 */ 1588 boolean onDialpadSpacerTouchWithEmptyQuery(); 1589 1590 /** Returns true if this fragment's parent want the dialpad to show the dialpad chooser. */ 1591 boolean shouldShowDialpadChooser(); 1592 } 1593 1594 /** 1595 * LinearLayout with getter and setter methods for the translationY property using floats, for 1596 * animation purposes. 1597 */ 1598 public static class DialpadSlidingRelativeLayout extends RelativeLayout { 1599 1600 public DialpadSlidingRelativeLayout(Context context) { 1601 super(context); 1602 } 1603 1604 public DialpadSlidingRelativeLayout(Context context, AttributeSet attrs) { 1605 super(context, attrs); 1606 } 1607 1608 public DialpadSlidingRelativeLayout(Context context, AttributeSet attrs, int defStyle) { 1609 super(context, attrs, defStyle); 1610 } 1611 1612 @UsedByReflection(value = "dialpad_fragment.xml") 1613 public float getYFraction() { 1614 final int height = getHeight(); 1615 if (height == 0) { 1616 return 0; 1617 } 1618 return getTranslationY() / height; 1619 } 1620 1621 @UsedByReflection(value = "dialpad_fragment.xml") 1622 public void setYFraction(float yFraction) { 1623 setTranslationY(yFraction * getHeight()); 1624 } 1625 } 1626 1627 public static class ErrorDialogFragment extends DialogFragment { 1628 1629 private static final String ARG_TITLE_RES_ID = "argTitleResId"; 1630 private static final String ARG_MESSAGE_RES_ID = "argMessageResId"; 1631 private int titleResId; 1632 private int messageResId; 1633 1634 public static ErrorDialogFragment newInstance(int messageResId) { 1635 return newInstance(0, messageResId); 1636 } 1637 1638 public static ErrorDialogFragment newInstance(int titleResId, int messageResId) { 1639 final ErrorDialogFragment fragment = new ErrorDialogFragment(); 1640 final Bundle args = new Bundle(); 1641 args.putInt(ARG_TITLE_RES_ID, titleResId); 1642 args.putInt(ARG_MESSAGE_RES_ID, messageResId); 1643 fragment.setArguments(args); 1644 return fragment; 1645 } 1646 1647 @Override 1648 public void onCreate(Bundle savedInstanceState) { 1649 super.onCreate(savedInstanceState); 1650 titleResId = getArguments().getInt(ARG_TITLE_RES_ID); 1651 messageResId = getArguments().getInt(ARG_MESSAGE_RES_ID); 1652 } 1653 1654 @Override 1655 public Dialog onCreateDialog(Bundle savedInstanceState) { 1656 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 1657 if (titleResId != 0) { 1658 builder.setTitle(titleResId); 1659 } 1660 if (messageResId != 0) { 1661 builder.setMessage(messageResId); 1662 } 1663 builder.setPositiveButton(android.R.string.ok, (dialog, which) -> dismiss()); 1664 return builder.create(); 1665 } 1666 } 1667 1668 /** 1669 * Simple list adapter, binding to an icon + text label for each item in the "dialpad chooser" 1670 * list. 1671 */ 1672 private static class DialpadChooserAdapter extends BaseAdapter { 1673 1674 // IDs for the possible "choices": 1675 static final int DIALPAD_CHOICE_USE_DTMF_DIALPAD = 101; 1676 static final int DIALPAD_CHOICE_RETURN_TO_CALL = 102; 1677 static final int DIALPAD_CHOICE_ADD_NEW_CALL = 103; 1678 private static final int NUM_ITEMS = 3; 1679 private LayoutInflater inflater; 1680 private ChoiceItem[] choiceItems = new ChoiceItem[NUM_ITEMS]; 1681 1682 DialpadChooserAdapter(Context context) { 1683 // Cache the LayoutInflate to avoid asking for a new one each time. 1684 inflater = LayoutInflater.from(context); 1685 1686 // Initialize the possible choices. 1687 // TODO: could this be specified entirely in XML? 1688 1689 // - "Use touch tone keypad" 1690 choiceItems[0] = 1691 new ChoiceItem( 1692 context.getString(R.string.dialer_useDtmfDialpad), 1693 BitmapFactory.decodeResource( 1694 context.getResources(), R.drawable.ic_dialer_fork_tt_keypad), 1695 DIALPAD_CHOICE_USE_DTMF_DIALPAD); 1696 1697 // - "Return to call in progress" 1698 choiceItems[1] = 1699 new ChoiceItem( 1700 context.getString(R.string.dialer_returnToInCallScreen), 1701 BitmapFactory.decodeResource( 1702 context.getResources(), R.drawable.ic_dialer_fork_current_call), 1703 DIALPAD_CHOICE_RETURN_TO_CALL); 1704 1705 // - "Add call" 1706 choiceItems[2] = 1707 new ChoiceItem( 1708 context.getString(R.string.dialer_addAnotherCall), 1709 BitmapFactory.decodeResource( 1710 context.getResources(), R.drawable.ic_dialer_fork_add_call), 1711 DIALPAD_CHOICE_ADD_NEW_CALL); 1712 } 1713 1714 @Override 1715 public int getCount() { 1716 return NUM_ITEMS; 1717 } 1718 1719 /** Return the ChoiceItem for a given position. */ 1720 @Override 1721 public Object getItem(int position) { 1722 return choiceItems[position]; 1723 } 1724 1725 /** Return a unique ID for each possible choice. */ 1726 @Override 1727 public long getItemId(int position) { 1728 return position; 1729 } 1730 1731 /** Make a view for each row. */ 1732 @Override 1733 public View getView(int position, View convertView, ViewGroup parent) { 1734 // When convertView is non-null, we can reuse it (there's no need 1735 // to reinflate it.) 1736 if (convertView == null) { 1737 convertView = inflater.inflate(R.layout.dialpad_chooser_list_item, null); 1738 } 1739 1740 TextView text = convertView.findViewById(R.id.text); 1741 text.setText(choiceItems[position].text); 1742 1743 ImageView icon = convertView.findViewById(R.id.icon); 1744 icon.setImageBitmap(choiceItems[position].icon); 1745 1746 return convertView; 1747 } 1748 1749 // Simple struct for a single "choice" item. 1750 static class ChoiceItem { 1751 1752 String text; 1753 Bitmap icon; 1754 int id; 1755 1756 ChoiceItem(String s, Bitmap b, int i) { 1757 text = s; 1758 icon = b; 1759 id = i; 1760 } 1761 } 1762 } 1763 1764 private class CallStateReceiver extends BroadcastReceiver { 1765 1766 /** 1767 * Receive call state changes so that we can take down the "dialpad chooser" if the phone 1768 * becomes idle while the chooser UI is visible. 1769 */ 1770 @Override 1771 public void onReceive(Context context, Intent intent) { 1772 String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE); 1773 if ((TextUtils.equals(state, TelephonyManager.EXTRA_STATE_IDLE) 1774 || TextUtils.equals(state, TelephonyManager.EXTRA_STATE_OFFHOOK)) 1775 && isDialpadChooserVisible()) { 1776 // Note there's a race condition in the UI here: the 1777 // dialpad chooser could conceivably disappear (on its 1778 // own) at the exact moment the user was trying to select 1779 // one of the choices, which would be confusing. (But at 1780 // least that's better than leaving the dialpad chooser 1781 // onscreen, but useless...) 1782 LogUtil.i("CallStateReceiver.onReceive", "hiding dialpad chooser, state: %s", state); 1783 showDialpadChooser(false); 1784 } 1785 } 1786 } 1787 1788 /** Listener for dialpad's parent. */ 1789 public interface DialpadListener { 1790 void getLastOutgoingCall(LastOutgoingCallCallback callback); 1791 1792 void onDialpadShown(); 1793 1794 void onCallPlacedFromDialpad(); 1795 } 1796 1797 /** Callback for async lookup of the last number dialed. */ 1798 public interface LastOutgoingCallCallback { 1799 1800 void lastOutgoingCall(String number); 1801 } 1802 1803 /** 1804 * A worker that helps formatting the phone number as the user types it in. 1805 * 1806 * <p>Input: the ISO 3166-1 two-letter country code of the country the user is in. 1807 * 1808 * <p>Output: an instance of {@link DialerPhoneNumberFormattingTextWatcher}. Note: It is unusual 1809 * to return a non-data value from a worker. But {@link DialerPhoneNumberFormattingTextWatcher} 1810 * depends on libphonenumber API, which cannot be initialized on the main thread. 1811 */ 1812 private static class InitPhoneNumberFormattingTextWatcherWorker 1813 implements Worker<String, DialerPhoneNumberFormattingTextWatcher> { 1814 1815 @Nullable 1816 @Override 1817 public DialerPhoneNumberFormattingTextWatcher doInBackground(@Nullable String countryCode) { 1818 return new DialerPhoneNumberFormattingTextWatcher(countryCode); 1819 } 1820 } 1821 1822 /** 1823 * An extension of Android telephony's {@link PhoneNumberFormattingTextWatcher}. This watcher 1824 * skips formatting Argentina mobile numbers for domestic calls. 1825 * 1826 * <p>As of Nov. 28, 2017, the as-you-type-formatting provided by libphonenumber's 1827 * AsYouTypeFormatter (which {@link PhoneNumberFormattingTextWatcher} depends on) can't correctly 1828 * format Argentina mobile numbers for domestic calls (a bug). We temporarily disable the 1829 * formatting for such numbers until libphonenumber is fixed (which will come as early as the next 1830 * Android release). 1831 */ 1832 @VisibleForTesting 1833 public static class DialerPhoneNumberFormattingTextWatcher 1834 extends PhoneNumberFormattingTextWatcher { 1835 private static final Pattern AR_DOMESTIC_CALL_MOBILE_NUMBER_PATTERN; 1836 1837 // This static initialization block builds a pattern for domestic calls to Argentina mobile 1838 // numbers: 1839 // (1) Local calls: 15 <local number> 1840 // (2) Long distance calls: <area code> 15 <local number> 1841 // See https://en.wikipedia.org/wiki/Telephone_numbers_in_Argentina for detailed explanations. 1842 static { 1843 String regex = 1844 "0?(" 1845 + " (" 1846 + " 11|" 1847 + " 2(" 1848 + " 2(" 1849 + " 02?|" 1850 + " [13]|" 1851 + " 2[13-79]|" 1852 + " 4[1-6]|" 1853 + " 5[2457]|" 1854 + " 6[124-8]|" 1855 + " 7[1-4]|" 1856 + " 8[13-6]|" 1857 + " 9[1267]" 1858 + " )|" 1859 + " 3(" 1860 + " 02?|" 1861 + " 1[467]|" 1862 + " 2[03-6]|" 1863 + " 3[13-8]|" 1864 + " [49][2-6]|" 1865 + " 5[2-8]|" 1866 + " [67]" 1867 + " )|" 1868 + " 4(" 1869 + " 7[3-578]|" 1870 + " 9" 1871 + " )|" 1872 + " 6(" 1873 + " [0136]|" 1874 + " 2[24-6]|" 1875 + " 4[6-8]?|" 1876 + " 5[15-8]" 1877 + " )|" 1878 + " 80|" 1879 + " 9(" 1880 + " 0[1-3]|" 1881 + " [19]|" 1882 + " 2\\d|" 1883 + " 3[1-6]|" 1884 + " 4[02568]?|" 1885 + " 5[2-4]|" 1886 + " 6[2-46]|" 1887 + " 72?|" 1888 + " 8[23]?" 1889 + " )" 1890 + " )|" 1891 + " 3(" 1892 + " 3(" 1893 + " 2[79]|" 1894 + " 6|" 1895 + " 8[2578]" 1896 + " )|" 1897 + " 4(" 1898 + " 0[0-24-9]|" 1899 + " [12]|" 1900 + " 3[5-8]?|" 1901 + " 4[24-7]|" 1902 + " 5[4-68]?|" 1903 + " 6[02-9]|" 1904 + " 7[126]|" 1905 + " 8[2379]?|" 1906 + " 9[1-36-8]" 1907 + " )|" 1908 + " 5(" 1909 + " 1|" 1910 + " 2[1245]|" 1911 + " 3[237]?|" 1912 + " 4[1-46-9]|" 1913 + " 6[2-4]|" 1914 + " 7[1-6]|" 1915 + " 8[2-5]?" 1916 + " )|" 1917 + " 6[24]|" 1918 + " 7(" 1919 + " [069]|" 1920 + " 1[1568]|" 1921 + " 2[15]|" 1922 + " 3[145]|" 1923 + " 4[13]|" 1924 + " 5[14-8]|" 1925 + " 7[2-57]|" 1926 + " 8[126]" 1927 + " )|" 1928 + " 8(" 1929 + " [01]|" 1930 + " 2[15-7]|" 1931 + " 3[2578]?|" 1932 + " 4[13-6]|" 1933 + " 5[4-8]?|" 1934 + " 6[1-357-9]|" 1935 + " 7[36-8]?|" 1936 + " 8[5-8]?|" 1937 + " 9[124]" 1938 + " )" 1939 + " )" 1940 + " )?15" 1941 + ").*"; 1942 AR_DOMESTIC_CALL_MOBILE_NUMBER_PATTERN = Pattern.compile(regex.replaceAll("\\s+", "")); 1943 } 1944 1945 private final String countryCode; 1946 1947 DialerPhoneNumberFormattingTextWatcher(String countryCode) { 1948 super(countryCode); 1949 this.countryCode = countryCode; 1950 } 1951 1952 @Override 1953 public synchronized void afterTextChanged(Editable s) { 1954 // When the country code is NOT "AR", Android telephony's PhoneNumberFormattingTextWatcher can 1955 // correctly handle the input so we will let it do its job. 1956 if (!Ascii.toUpperCase(countryCode).equals("AR")) { 1957 super.afterTextChanged(s); 1958 return; 1959 } 1960 1961 // When the country code is "AR", PhoneNumberFormattingTextWatcher can also format the input 1962 // correctly if the number is NOT for a domestic call to a mobile phone. 1963 String rawNumber = getRawNumber(s); 1964 Matcher matcher = AR_DOMESTIC_CALL_MOBILE_NUMBER_PATTERN.matcher(rawNumber); 1965 if (!matcher.matches()) { 1966 super.afterTextChanged(s); 1967 return; 1968 } 1969 1970 // As modifying the input will trigger another call to afterTextChanged(Editable), we must 1971 // check whether the input's format has already been removed and return if it has 1972 // been to avoid infinite recursion. 1973 if (rawNumber.contentEquals(s)) { 1974 return; 1975 } 1976 1977 // If we reach this point, the country code must be "AR" and variable "s" represents a number 1978 // for a domestic call to a mobile phone. "s" is incorrectly formatted by Android telephony's 1979 // PhoneNumberFormattingTextWatcher so we remove its format by replacing it with the raw 1980 // number. 1981 s.replace(0, s.length(), rawNumber); 1982 1983 // Make sure the cursor is at the end of the text. 1984 Selection.setSelection(s, s.length()); 1985 1986 PhoneNumberUtils.addTtsSpan(s, 0 /* start */, s.length() /* endExclusive */); 1987 } 1988 1989 private static String getRawNumber(Editable s) { 1990 StringBuilder rawNumberBuilder = new StringBuilder(); 1991 1992 for (int i = 0; i < s.length(); i++) { 1993 char c = s.charAt(i); 1994 if (PhoneNumberUtils.isNonSeparator(c)) { 1995 rawNumberBuilder.append(c); 1996 } 1997 } 1998 1999 return rawNumberBuilder.toString(); 2000 } 2001 } 2002 } 2003