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