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