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