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