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