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