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