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