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