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