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