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