Home | History | Annotate | Download | only in phone
      1 /*
      2  * Copyright (C) 2008 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.phone;
     18 
     19 import android.app.Activity;
     20 import android.app.AlertDialog;
     21 import android.app.Dialog;
     22 import android.app.StatusBarManager;
     23 import android.content.BroadcastReceiver;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.content.IntentFilter;
     27 import android.content.res.Resources;
     28 import android.media.AudioManager;
     29 import android.media.ToneGenerator;
     30 import android.net.Uri;
     31 import android.os.Bundle;
     32 import android.provider.Settings;
     33 import android.telephony.PhoneNumberUtils;
     34 import android.text.Editable;
     35 import android.text.TextUtils;
     36 import android.text.TextWatcher;
     37 import android.text.method.DialerKeyListener;
     38 import android.util.Log;
     39 import android.view.KeyEvent;
     40 import android.view.MotionEvent;
     41 import android.view.View;
     42 import android.view.WindowManager;
     43 import android.view.accessibility.AccessibilityManager;
     44 import android.widget.EditText;
     45 
     46 import com.android.phone.common.HapticFeedback;
     47 
     48 
     49 /**
     50  * EmergencyDialer is a special dialer that is used ONLY for dialing emergency calls.
     51  *
     52  * It's a simplified version of the regular dialer (i.e. the TwelveKeyDialer
     53  * activity from apps/Contacts) that:
     54  *   1. Allows ONLY emergency calls to be dialed
     55  *   2. Disallows voicemail functionality
     56  *   3. Uses the FLAG_SHOW_WHEN_LOCKED window manager flag to allow this
     57  *      activity to stay in front of the keyguard.
     58  *
     59  * TODO: Even though this is an ultra-simplified version of the normal
     60  * dialer, there's still lots of code duplication between this class and
     61  * the TwelveKeyDialer class from apps/Contacts.  Could the common code be
     62  * moved into a shared base class that would live in the framework?
     63  * Or could we figure out some way to move *this* class into apps/Contacts
     64  * also?
     65  */
     66 public class EmergencyDialer extends Activity implements View.OnClickListener,
     67         View.OnLongClickListener, View.OnHoverListener, View.OnKeyListener, TextWatcher {
     68     // Keys used with onSaveInstanceState().
     69     private static final String LAST_NUMBER = "lastNumber";
     70 
     71     // Intent action for this activity.
     72     public static final String ACTION_DIAL = "com.android.phone.EmergencyDialer.DIAL";
     73 
     74     // List of dialer button IDs.
     75     private static final int[] DIALER_KEYS = new int[] {
     76             R.id.one, R.id.two, R.id.three,
     77             R.id.four, R.id.five, R.id.six,
     78             R.id.seven, R.id.eight, R.id.nine,
     79             R.id.star, R.id.zero, R.id.pound };
     80 
     81     // Debug constants.
     82     private static final boolean DBG = false;
     83     private static final String LOG_TAG = "EmergencyDialer";
     84 
     85     private PhoneGlobals mApp;
     86     private StatusBarManager mStatusBarManager;
     87     private AccessibilityManager mAccessibilityManager;
     88 
     89     /** The length of DTMF tones in milliseconds */
     90     private static final int TONE_LENGTH_MS = 150;
     91 
     92     /** The DTMF tone volume relative to other sounds in the stream */
     93     private static final int TONE_RELATIVE_VOLUME = 80;
     94 
     95     /** Stream type used to play the DTMF tones off call, and mapped to the volume control keys */
     96     private static final int DIAL_TONE_STREAM_TYPE = AudioManager.STREAM_DTMF;
     97 
     98     private static final int BAD_EMERGENCY_NUMBER_DIALOG = 0;
     99 
    100     private static final int USER_ACTIVITY_TIMEOUT_WHEN_NO_PROX_SENSOR = 15000; // millis
    101 
    102     EditText mDigits;
    103     private View mDialButton;
    104     private View mDelete;
    105 
    106     private ToneGenerator mToneGenerator;
    107     private Object mToneGeneratorLock = new Object();
    108 
    109     // determines if we want to playback local DTMF tones.
    110     private boolean mDTMFToneEnabled;
    111 
    112     // Haptic feedback (vibration) for dialer key presses.
    113     private HapticFeedback mHaptic = new HapticFeedback();
    114 
    115     // close activity when screen turns off
    116     private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
    117         @Override
    118         public void onReceive(Context context, Intent intent) {
    119             if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
    120                 finish();
    121             }
    122         }
    123     };
    124 
    125     private String mLastNumber; // last number we tried to dial. Used to restore error dialog.
    126 
    127     @Override
    128     public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    129         // Do nothing
    130     }
    131 
    132     @Override
    133     public void onTextChanged(CharSequence input, int start, int before, int changeCount) {
    134         // Do nothing
    135     }
    136 
    137     @Override
    138     public void afterTextChanged(Editable input) {
    139         // Check for special sequences, in particular the "**04" or "**05"
    140         // sequences that allow you to enter PIN or PUK-related codes.
    141         //
    142         // But note we *don't* allow most other special sequences here,
    143         // like "secret codes" (*#*#<code>#*#*) or IMEI display ("*#06#"),
    144         // since those shouldn't be available if the device is locked.
    145         //
    146         // So we call SpecialCharSequenceMgr.handleCharsForLockedDevice()
    147         // here, not the regular handleChars() method.
    148         if (SpecialCharSequenceMgr.handleCharsForLockedDevice(this, input.toString(), this)) {
    149             // A special sequence was entered, clear the digits
    150             mDigits.getText().clear();
    151         }
    152 
    153         updateDialAndDeleteButtonStateEnabledAttr();
    154     }
    155 
    156     @Override
    157     protected void onCreate(Bundle icicle) {
    158         super.onCreate(icicle);
    159 
    160         mApp = PhoneGlobals.getInstance();
    161         mStatusBarManager = (StatusBarManager) getSystemService(Context.STATUS_BAR_SERVICE);
    162         mAccessibilityManager = (AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE);
    163 
    164         // Allow this activity to be displayed in front of the keyguard / lockscreen.
    165         WindowManager.LayoutParams lp = getWindow().getAttributes();
    166         lp.flags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
    167         if (!mApp.proximitySensorModeEnabled()) {
    168             // When no proximity sensor is available, use a shorter timeout.
    169             lp.userActivityTimeout = USER_ACTIVITY_TIMEOUT_WHEN_NO_PROX_SENSOR;
    170         }
    171         getWindow().setAttributes(lp);
    172 
    173         setContentView(R.layout.emergency_dialer);
    174 
    175         mDigits = (EditText) findViewById(R.id.digits);
    176         mDigits.setKeyListener(DialerKeyListener.getInstance());
    177         mDigits.setOnClickListener(this);
    178         mDigits.setOnKeyListener(this);
    179         mDigits.setLongClickable(false);
    180         if (mAccessibilityManager.isEnabled()) {
    181             // The text view must be selected to send accessibility events.
    182             mDigits.setSelected(true);
    183         }
    184         maybeAddNumberFormatting();
    185 
    186         // Check for the presence of the keypad
    187         View view = findViewById(R.id.one);
    188         if (view != null) {
    189             setupKeypad();
    190         }
    191 
    192         mDelete = findViewById(R.id.deleteButton);
    193         mDelete.setOnClickListener(this);
    194         mDelete.setOnLongClickListener(this);
    195 
    196         mDialButton = findViewById(R.id.dialButton);
    197 
    198         // Check whether we should show the onscreen "Dial" button and co.
    199         Resources res = getResources();
    200         if (res.getBoolean(R.bool.config_show_onscreen_dial_button)) {
    201             mDialButton.setOnClickListener(this);
    202         } else {
    203             mDialButton.setVisibility(View.GONE);
    204         }
    205 
    206         if (icicle != null) {
    207             super.onRestoreInstanceState(icicle);
    208         }
    209 
    210         // Extract phone number from intent
    211         Uri data = getIntent().getData();
    212         if (data != null && (Constants.SCHEME_TEL.equals(data.getScheme()))) {
    213             String number = PhoneNumberUtils.getNumberFromIntent(getIntent(), this);
    214             if (number != null) {
    215                 mDigits.setText(number);
    216             }
    217         }
    218 
    219         // if the mToneGenerator creation fails, just continue without it.  It is
    220         // a local audio signal, and is not as important as the dtmf tone itself.
    221         synchronized (mToneGeneratorLock) {
    222             if (mToneGenerator == null) {
    223                 try {
    224                     mToneGenerator = new ToneGenerator(DIAL_TONE_STREAM_TYPE, TONE_RELATIVE_VOLUME);
    225                 } catch (RuntimeException e) {
    226                     Log.w(LOG_TAG, "Exception caught while creating local tone generator: " + e);
    227                     mToneGenerator = null;
    228                 }
    229             }
    230         }
    231 
    232         final IntentFilter intentFilter = new IntentFilter();
    233         intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
    234         registerReceiver(mBroadcastReceiver, intentFilter);
    235 
    236         try {
    237             mHaptic.init(this, res.getBoolean(R.bool.config_enable_dialer_key_vibration));
    238         } catch (Resources.NotFoundException nfe) {
    239              Log.e(LOG_TAG, "Vibrate control bool missing.", nfe);
    240         }
    241     }
    242 
    243     @Override
    244     protected void onDestroy() {
    245         super.onDestroy();
    246         synchronized (mToneGeneratorLock) {
    247             if (mToneGenerator != null) {
    248                 mToneGenerator.release();
    249                 mToneGenerator = null;
    250             }
    251         }
    252         unregisterReceiver(mBroadcastReceiver);
    253     }
    254 
    255     @Override
    256     protected void onRestoreInstanceState(Bundle icicle) {
    257         mLastNumber = icicle.getString(LAST_NUMBER);
    258     }
    259 
    260     @Override
    261     protected void onSaveInstanceState(Bundle outState) {
    262         super.onSaveInstanceState(outState);
    263         outState.putString(LAST_NUMBER, mLastNumber);
    264     }
    265 
    266     /**
    267      * Explicitly turn off number formatting, since it gets in the way of the emergency
    268      * number detector
    269      */
    270     protected void maybeAddNumberFormatting() {
    271         // Do nothing.
    272     }
    273 
    274     @Override
    275     protected void onPostCreate(Bundle savedInstanceState) {
    276         super.onPostCreate(savedInstanceState);
    277 
    278         // This can't be done in onCreate(), since the auto-restoring of the digits
    279         // will play DTMF tones for all the old digits if it is when onRestoreSavedInstanceState()
    280         // is called. This method will be called every time the activity is created, and
    281         // will always happen after onRestoreSavedInstanceState().
    282         mDigits.addTextChangedListener(this);
    283     }
    284 
    285     private void setupKeypad() {
    286         // Setup the listeners for the buttons
    287         for (int id : DIALER_KEYS) {
    288             final View key = findViewById(id);
    289             key.setOnClickListener(this);
    290             key.setOnHoverListener(this);
    291         }
    292 
    293         View view = findViewById(R.id.zero);
    294         view.setOnLongClickListener(this);
    295     }
    296 
    297     /**
    298      * handle key events
    299      */
    300     @Override
    301     public boolean onKeyDown(int keyCode, KeyEvent event) {
    302         switch (keyCode) {
    303             // Happen when there's a "Call" hard button.
    304             case KeyEvent.KEYCODE_CALL: {
    305                 if (TextUtils.isEmpty(mDigits.getText().toString())) {
    306                     // if we are adding a call from the InCallScreen and the phone
    307                     // number entered is empty, we just close the dialer to expose
    308                     // the InCallScreen under it.
    309                     finish();
    310                 } else {
    311                     // otherwise, we place the call.
    312                     placeCall();
    313                 }
    314                 return true;
    315             }
    316         }
    317         return super.onKeyDown(keyCode, event);
    318     }
    319 
    320     private void keyPressed(int keyCode) {
    321         mHaptic.vibrate();
    322         KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
    323         mDigits.onKeyDown(keyCode, event);
    324     }
    325 
    326     @Override
    327     public boolean onKey(View view, int keyCode, KeyEvent event) {
    328         switch (view.getId()) {
    329             case R.id.digits:
    330                 // Happen when "Done" button of the IME is pressed. This can happen when this
    331                 // Activity is forced into landscape mode due to a desk dock.
    332                 if (keyCode == KeyEvent.KEYCODE_ENTER
    333                         && event.getAction() == KeyEvent.ACTION_UP) {
    334                     placeCall();
    335                     return true;
    336                 }
    337                 break;
    338         }
    339         return false;
    340     }
    341 
    342     @Override
    343     public void onClick(View view) {
    344         switch (view.getId()) {
    345             case R.id.one: {
    346                 playTone(ToneGenerator.TONE_DTMF_1);
    347                 keyPressed(KeyEvent.KEYCODE_1);
    348                 return;
    349             }
    350             case R.id.two: {
    351                 playTone(ToneGenerator.TONE_DTMF_2);
    352                 keyPressed(KeyEvent.KEYCODE_2);
    353                 return;
    354             }
    355             case R.id.three: {
    356                 playTone(ToneGenerator.TONE_DTMF_3);
    357                 keyPressed(KeyEvent.KEYCODE_3);
    358                 return;
    359             }
    360             case R.id.four: {
    361                 playTone(ToneGenerator.TONE_DTMF_4);
    362                 keyPressed(KeyEvent.KEYCODE_4);
    363                 return;
    364             }
    365             case R.id.five: {
    366                 playTone(ToneGenerator.TONE_DTMF_5);
    367                 keyPressed(KeyEvent.KEYCODE_5);
    368                 return;
    369             }
    370             case R.id.six: {
    371                 playTone(ToneGenerator.TONE_DTMF_6);
    372                 keyPressed(KeyEvent.KEYCODE_6);
    373                 return;
    374             }
    375             case R.id.seven: {
    376                 playTone(ToneGenerator.TONE_DTMF_7);
    377                 keyPressed(KeyEvent.KEYCODE_7);
    378                 return;
    379             }
    380             case R.id.eight: {
    381                 playTone(ToneGenerator.TONE_DTMF_8);
    382                 keyPressed(KeyEvent.KEYCODE_8);
    383                 return;
    384             }
    385             case R.id.nine: {
    386                 playTone(ToneGenerator.TONE_DTMF_9);
    387                 keyPressed(KeyEvent.KEYCODE_9);
    388                 return;
    389             }
    390             case R.id.zero: {
    391                 playTone(ToneGenerator.TONE_DTMF_0);
    392                 keyPressed(KeyEvent.KEYCODE_0);
    393                 return;
    394             }
    395             case R.id.pound: {
    396                 playTone(ToneGenerator.TONE_DTMF_P);
    397                 keyPressed(KeyEvent.KEYCODE_POUND);
    398                 return;
    399             }
    400             case R.id.star: {
    401                 playTone(ToneGenerator.TONE_DTMF_S);
    402                 keyPressed(KeyEvent.KEYCODE_STAR);
    403                 return;
    404             }
    405             case R.id.deleteButton: {
    406                 keyPressed(KeyEvent.KEYCODE_DEL);
    407                 return;
    408             }
    409             case R.id.dialButton: {
    410                 mHaptic.vibrate();  // Vibrate here too, just like we do for the regular keys
    411                 placeCall();
    412                 return;
    413             }
    414             case R.id.digits: {
    415                 if (mDigits.length() != 0) {
    416                     mDigits.setCursorVisible(true);
    417                 }
    418                 return;
    419             }
    420         }
    421     }
    422 
    423     /**
    424      * Implemented for {@link android.view.View.OnHoverListener}. Handles touch
    425      * events for accessibility when touch exploration is enabled.
    426      */
    427     @Override
    428     public boolean onHover(View v, MotionEvent event) {
    429         // When touch exploration is turned on, lifting a finger while inside
    430         // the button's hover target bounds should perform a click action.
    431         if (mAccessibilityManager.isEnabled()
    432                 && mAccessibilityManager.isTouchExplorationEnabled()) {
    433 
    434             switch (event.getActionMasked()) {
    435                 case MotionEvent.ACTION_HOVER_ENTER:
    436                     // Lift-to-type temporarily disables double-tap activation.
    437                     v.setClickable(false);
    438                     break;
    439                 case MotionEvent.ACTION_HOVER_EXIT:
    440                     final int left = v.getPaddingLeft();
    441                     final int right = (v.getWidth() - v.getPaddingRight());
    442                     final int top = v.getPaddingTop();
    443                     final int bottom = (v.getHeight() - v.getPaddingBottom());
    444                     final int x = (int) event.getX();
    445                     final int y = (int) event.getY();
    446                     if ((x > left) && (x < right) && (y > top) && (y < bottom)) {
    447                         v.performClick();
    448                     }
    449                     v.setClickable(true);
    450                     break;
    451             }
    452         }
    453 
    454         return false;
    455     }
    456 
    457     /**
    458      * called for long touch events
    459      */
    460     @Override
    461     public boolean onLongClick(View view) {
    462         int id = view.getId();
    463         switch (id) {
    464             case R.id.deleteButton: {
    465                 mDigits.getText().clear();
    466                 // TODO: The framework forgets to clear the pressed
    467                 // status of disabled button. Until this is fixed,
    468                 // clear manually the pressed status. b/2133127
    469                 mDelete.setPressed(false);
    470                 return true;
    471             }
    472             case R.id.zero: {
    473                 keyPressed(KeyEvent.KEYCODE_PLUS);
    474                 return true;
    475             }
    476         }
    477         return false;
    478     }
    479 
    480     @Override
    481     protected void onResume() {
    482         super.onResume();
    483 
    484         // retrieve the DTMF tone play back setting.
    485         mDTMFToneEnabled = Settings.System.getInt(getContentResolver(),
    486                 Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1;
    487 
    488         // Retrieve the haptic feedback setting.
    489         mHaptic.checkSystemSetting();
    490 
    491         // if the mToneGenerator creation fails, just continue without it.  It is
    492         // a local audio signal, and is not as important as the dtmf tone itself.
    493         synchronized (mToneGeneratorLock) {
    494             if (mToneGenerator == null) {
    495                 try {
    496                     mToneGenerator = new ToneGenerator(AudioManager.STREAM_DTMF,
    497                             TONE_RELATIVE_VOLUME);
    498                 } catch (RuntimeException e) {
    499                     Log.w(LOG_TAG, "Exception caught while creating local tone generator: " + e);
    500                     mToneGenerator = null;
    501                 }
    502             }
    503         }
    504 
    505         // Disable the status bar and set the poke lock timeout to medium.
    506         // There is no need to do anything with the wake lock.
    507         if (DBG) Log.d(LOG_TAG, "disabling status bar, set to long timeout");
    508         mStatusBarManager.disable(StatusBarManager.DISABLE_EXPAND);
    509 
    510         updateDialAndDeleteButtonStateEnabledAttr();
    511     }
    512 
    513     @Override
    514     public void onPause() {
    515         // Reenable the status bar and set the poke lock timeout to default.
    516         // There is no need to do anything with the wake lock.
    517         if (DBG) Log.d(LOG_TAG, "reenabling status bar and closing the dialer");
    518         mStatusBarManager.disable(StatusBarManager.DISABLE_NONE);
    519 
    520         super.onPause();
    521 
    522         synchronized (mToneGeneratorLock) {
    523             if (mToneGenerator != null) {
    524                 mToneGenerator.release();
    525                 mToneGenerator = null;
    526             }
    527         }
    528     }
    529 
    530     /**
    531      * place the call, but check to make sure it is a viable number.
    532      */
    533     private void placeCall() {
    534         mLastNumber = mDigits.getText().toString();
    535         if (PhoneNumberUtils.isLocalEmergencyNumber(mLastNumber, this)) {
    536             if (DBG) Log.d(LOG_TAG, "placing call to " + mLastNumber);
    537 
    538             // place the call if it is a valid number
    539             if (mLastNumber == null || !TextUtils.isGraphic(mLastNumber)) {
    540                 // There is no number entered.
    541                 playTone(ToneGenerator.TONE_PROP_NACK);
    542                 return;
    543             }
    544             Intent intent = new Intent(Intent.ACTION_CALL_EMERGENCY);
    545             intent.setData(Uri.fromParts(Constants.SCHEME_TEL, mLastNumber, null));
    546             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    547             startActivity(intent);
    548             finish();
    549         } else {
    550             if (DBG) Log.d(LOG_TAG, "rejecting bad requested number " + mLastNumber);
    551 
    552             // erase the number and throw up an alert dialog.
    553             mDigits.getText().delete(0, mDigits.getText().length());
    554             showDialog(BAD_EMERGENCY_NUMBER_DIALOG);
    555         }
    556     }
    557 
    558     /**
    559      * Plays the specified tone for TONE_LENGTH_MS milliseconds.
    560      *
    561      * The tone is played locally, using the audio stream for phone calls.
    562      * Tones are played only if the "Audible touch tones" user preference
    563      * is checked, and are NOT played if the device is in silent mode.
    564      *
    565      * @param tone a tone code from {@link ToneGenerator}
    566      */
    567     void playTone(int tone) {
    568         // if local tone playback is disabled, just return.
    569         if (!mDTMFToneEnabled) {
    570             return;
    571         }
    572 
    573         // Also do nothing if the phone is in silent mode.
    574         // We need to re-check the ringer mode for *every* playTone()
    575         // call, rather than keeping a local flag that's updated in
    576         // onResume(), since it's possible to toggle silent mode without
    577         // leaving the current activity (via the ENDCALL-longpress menu.)
    578         AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
    579         int ringerMode = audioManager.getRingerMode();
    580         if ((ringerMode == AudioManager.RINGER_MODE_SILENT)
    581             || (ringerMode == AudioManager.RINGER_MODE_VIBRATE)) {
    582             return;
    583         }
    584 
    585         synchronized (mToneGeneratorLock) {
    586             if (mToneGenerator == null) {
    587                 Log.w(LOG_TAG, "playTone: mToneGenerator == null, tone: " + tone);
    588                 return;
    589             }
    590 
    591             // Start the new tone (will stop any playing tone)
    592             mToneGenerator.startTone(tone, TONE_LENGTH_MS);
    593         }
    594     }
    595 
    596     private CharSequence createErrorMessage(String number) {
    597         if (!TextUtils.isEmpty(number)) {
    598             return getString(R.string.dial_emergency_error, mLastNumber);
    599         } else {
    600             return getText(R.string.dial_emergency_empty_error).toString();
    601         }
    602     }
    603 
    604     @Override
    605     protected Dialog onCreateDialog(int id) {
    606         AlertDialog dialog = null;
    607         if (id == BAD_EMERGENCY_NUMBER_DIALOG) {
    608             // construct dialog
    609             dialog = new AlertDialog.Builder(this)
    610                     .setTitle(getText(R.string.emergency_enable_radio_dialog_title))
    611                     .setMessage(createErrorMessage(mLastNumber))
    612                     .setPositiveButton(R.string.ok, null)
    613                     .setCancelable(true).create();
    614 
    615             // blur stuff behind the dialog
    616             dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
    617         }
    618         return dialog;
    619     }
    620 
    621     @Override
    622     protected void onPrepareDialog(int id, Dialog dialog) {
    623         super.onPrepareDialog(id, dialog);
    624         if (id == BAD_EMERGENCY_NUMBER_DIALOG) {
    625             AlertDialog alert = (AlertDialog) dialog;
    626             alert.setMessage(createErrorMessage(mLastNumber));
    627         }
    628     }
    629 
    630     /**
    631      * Update the enabledness of the "Dial" and "Backspace" buttons if applicable.
    632      */
    633     private void updateDialAndDeleteButtonStateEnabledAttr() {
    634         final boolean notEmpty = mDigits.length() != 0;
    635 
    636         mDialButton.setEnabled(notEmpty);
    637         mDelete.setEnabled(notEmpty);
    638     }
    639 }
    640