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