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