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