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