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