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