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