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