• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
beforeTextChanged(CharSequence s, int start, int count, int after)131     public void beforeTextChanged(CharSequence s, int start, int count, int after) {
132         // Do nothing
133     }
134 
135     @Override
onTextChanged(CharSequence input, int start, int before, int changeCount)136     public void onTextChanged(CharSequence input, int start, int before, int changeCount) {
137         // Do nothing
138     }
139 
140     @Override
afterTextChanged(Editable input)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
onCreate(Bundle icicle)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
onDestroy()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
onRestoreInstanceState(Bundle icicle)261     protected void onRestoreInstanceState(Bundle icicle) {
262         mLastNumber = icicle.getString(LAST_NUMBER);
263     }
264 
265     @Override
onSaveInstanceState(Bundle outState)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      */
maybeAddNumberFormatting()275     protected void maybeAddNumberFormatting() {
276         // Do nothing.
277     }
278 
279     @Override
onPostCreate(Bundle savedInstanceState)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 
setupKeypad()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
onKeyDown(int keyCode, KeyEvent event)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 
keyPressed(int keyCode)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
onKey(View view, int keyCode, KeyEvent event)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
onClick(View view)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
onPressed(View view, boolean pressed)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
onLongClick(View view)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
onResume()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
onPause()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      */
placeCall()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      */
playTone(int tone)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 
createErrorMessage(String number)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
onCreateDialog(int id)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
onPrepareDialog(int id, Dialog dialog)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
onOptionsItemSelected(MenuItem item)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      */
updateDialAndDeleteButtonStateEnabledAttr()623     private void updateDialAndDeleteButtonStateEnabledAttr() {
624         final boolean notEmpty = mDigits.length() != 0;
625 
626         mDelete.setEnabled(notEmpty);
627     }
628 }
629