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