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