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