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