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