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