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 static android.telephony.ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorListenerAdapter; 23 import android.app.Activity; 24 import android.app.AlertDialog; 25 import android.app.Dialog; 26 import android.app.WallpaperColors; 27 import android.app.WallpaperManager; 28 import android.content.BroadcastReceiver; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.IntentFilter; 32 import android.database.DataSetObserver; 33 import android.graphics.Color; 34 import android.graphics.Point; 35 import android.graphics.drawable.ColorDrawable; 36 import android.media.AudioManager; 37 import android.media.ToneGenerator; 38 import android.net.Uri; 39 import android.os.AsyncTask; 40 import android.os.Bundle; 41 import android.os.PersistableBundle; 42 import android.provider.Settings; 43 import android.telecom.PhoneAccount; 44 import android.telecom.TelecomManager; 45 import android.telephony.CarrierConfigManager; 46 import android.telephony.PhoneNumberUtils; 47 import com.android.telephony.Rlog; 48 import android.telephony.ServiceState; 49 import android.telephony.SubscriptionManager; 50 import android.telephony.TelephonyManager; 51 import android.text.Editable; 52 import android.text.InputType; 53 import android.text.Spannable; 54 import android.text.SpannableString; 55 import android.text.TextUtils; 56 import android.text.TextWatcher; 57 import android.text.method.DialerKeyListener; 58 import android.text.style.TtsSpan; 59 import android.util.Log; 60 import android.util.TypedValue; 61 import android.view.HapticFeedbackConstants; 62 import android.view.KeyEvent; 63 import android.view.MenuItem; 64 import android.view.MotionEvent; 65 import android.view.View; 66 import android.view.View.AccessibilityDelegate; 67 import android.view.ViewGroup; 68 import android.view.WindowManager; 69 import android.view.accessibility.AccessibilityEvent; 70 import android.widget.TextView; 71 72 import com.android.phone.common.dialpad.DialpadKeyButton; 73 import com.android.phone.common.util.ViewUtil; 74 import com.android.phone.common.widget.ResizingTextEditText; 75 76 import java.util.ArrayList; 77 import java.util.List; 78 import java.util.Locale; 79 80 /** 81 * EmergencyDialer is a special dialer that is used ONLY for dialing emergency calls. 82 * 83 * It's a simplified version of the regular dialer (i.e. the TwelveKeyDialer 84 * activity from apps/Contacts) that: 85 * 1. Allows ONLY emergency calls to be dialed 86 * 2. Disallows voicemail functionality 87 * 3. Allows this activity to stay in front of the keyguard. 88 * 89 * TODO: Even though this is an ultra-simplified version of the normal 90 * dialer, there's still lots of code duplication between this class and 91 * the TwelveKeyDialer class from apps/Contacts. Could the common code be 92 * moved into a shared base class that would live in the framework? 93 * Or could we figure out some way to move *this* class into apps/Contacts 94 * also? 95 */ 96 public class EmergencyDialer extends Activity implements View.OnClickListener, 97 View.OnLongClickListener, View.OnKeyListener, TextWatcher, 98 DialpadKeyButton.OnPressedListener, 99 WallpaperManager.OnColorsChangedListener, 100 EmergencyShortcutButton.OnConfirmClickListener, 101 EmergencyInfoGroup.OnConfirmClickListener { 102 103 // Keys used with onSaveInstanceState(). 104 private static final String LAST_NUMBER = "lastNumber"; 105 106 // Intent action for this activity. 107 public static final String ACTION_DIAL = "com.android.phone.EmergencyDialer.DIAL"; 108 109 /** 110 * Extra included in {@link #ACTION_DIAL} to indicate the entry type that user starts 111 * the emergency dialer. 112 */ 113 public static final String EXTRA_ENTRY_TYPE = 114 "com.android.phone.EmergencyDialer.extra.ENTRY_TYPE"; 115 116 // Constants indicating the entry type that user opened emergency dialer. 117 // This info is sent from system UI with EXTRA_ENTRY_TYPE. Please make them being 118 // in sync with those in com.android.systemui.util.EmergencyDialerConstants. 119 public static final int ENTRY_TYPE_UNKNOWN = 0; 120 public static final int ENTRY_TYPE_LOCKSCREEN_BUTTON = 1; 121 public static final int ENTRY_TYPE_POWER_MENU = 2; 122 123 // List of dialer button IDs. 124 private static final int[] DIALER_KEYS = new int[]{ 125 R.id.one, R.id.two, R.id.three, 126 R.id.four, R.id.five, R.id.six, 127 R.id.seven, R.id.eight, R.id.nine, 128 R.id.star, R.id.zero, R.id.pound}; 129 130 // Debug constants. 131 private static final boolean DBG = false; 132 private static final String LOG_TAG = "EmergencyDialer"; 133 134 /** The length of DTMF tones in milliseconds */ 135 private static final int TONE_LENGTH_MS = 150; 136 137 /** The DTMF tone volume relative to other sounds in the stream */ 138 private static final int TONE_RELATIVE_VOLUME = 80; 139 140 /** Stream type used to play the DTMF tones off call, and mapped to the volume control keys */ 141 private static final int DIAL_TONE_STREAM_TYPE = AudioManager.STREAM_DTMF; 142 143 private static final int BAD_EMERGENCY_NUMBER_DIALOG = 0; 144 145 /** 90% opacity, different from other gradients **/ 146 private static final int BACKGROUND_GRADIENT_ALPHA = 230; 147 148 /** 85% opacity for black background **/ 149 private static final int BLACK_BACKGROUND_GRADIENT_ALPHA = 217; 150 151 /** Size limit of emergency shortcut buttons container. **/ 152 private static final int SHORTCUT_SIZE_LIMIT = 3; 153 154 ResizingTextEditText mDigits; 155 private View mDialButton; 156 private View mDelete; 157 private View mEmergencyShortcutView; 158 private View mDialpadView; 159 160 private List<EmergencyShortcutButton> mEmergencyShortcutButtonList; 161 private EccShortcutAdapter mShortcutAdapter; 162 private DataSetObserver mShortcutDataSetObserver = null; 163 164 private ToneGenerator mToneGenerator; 165 private Object mToneGeneratorLock = new Object(); 166 167 // determines if we want to playback local DTMF tones. 168 private boolean mDTMFToneEnabled; 169 170 private EmergencyActionGroup mEmergencyActionGroup; 171 172 private EmergencyInfoGroup mEmergencyInfoGroup; 173 174 // close activity when screen turns off 175 private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 176 @Override 177 public void onReceive(Context context, Intent intent) { 178 if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) { 179 finishAndRemoveTask(); 180 } 181 } 182 }; 183 184 /** 185 * Customize accessibility methods in View. 186 */ 187 private AccessibilityDelegate mAccessibilityDelegate = new AccessibilityDelegate() { 188 189 /** 190 * Stop AccessiblityService from reading the title of a hidden View. 191 * 192 * <p>The crossfade animation will set the visibility of fade out view to {@link View.GONE} 193 * in the animation end. The view with an accessibility pane title would call the 194 * {@link AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED} event, which would trigger the 195 * accessibility service to read the pane title of fade out view instead of pane title of 196 * fade in view. So it need to filter out the event called by vanished pane. 197 */ 198 @Override 199 public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { 200 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED 201 && host.getVisibility() == View.GONE) { 202 return; 203 } 204 super.onPopulateAccessibilityEvent(host, event); 205 } 206 }; 207 208 private String mLastNumber; // last number we tried to dial. Used to restore error dialog. 209 210 // Background gradient 211 private ColorDrawable mBackgroundDrawable; 212 private boolean mSupportsDarkText; 213 214 private boolean mIsWfcEmergencyCallingWarningEnabled; 215 private float mDefaultDigitsTextSize; 216 217 private int mEntryType; 218 private ShortcutViewUtils.Config mShortcutViewConfig; 219 220 @Override beforeTextChanged(CharSequence s, int start, int count, int after)221 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 222 // Do nothing 223 } 224 225 @Override onTextChanged(CharSequence input, int start, int before, int changeCount)226 public void onTextChanged(CharSequence input, int start, int before, int changeCount) { 227 maybeChangeHintSize(); 228 } 229 230 @Override afterTextChanged(Editable input)231 public void afterTextChanged(Editable input) { 232 // Check for special sequences, in particular the "**04" or "**05" 233 // sequences that allow you to enter PIN or PUK-related codes. 234 // 235 // But note we *don't* allow most other special sequences here, 236 // like "secret codes" (*#*#<code>#*#*) or IMEI display ("*#06#"), 237 // since those shouldn't be available if the device is locked. 238 // 239 // So we call SpecialCharSequenceMgr.handleCharsForLockedDevice() 240 // here, not the regular handleChars() method. 241 if (SpecialCharSequenceMgr.handleCharsForLockedDevice(this, input.toString(), this)) { 242 // A special sequence was entered, clear the digits 243 mDigits.getText().clear(); 244 } 245 246 updateDialAndDeleteButtonStateEnabledAttr(); 247 updateTtsSpans(); 248 } 249 250 @Override onCreate(Bundle icicle)251 protected void onCreate(Bundle icicle) { 252 super.onCreate(icicle); 253 254 mEntryType = getIntent().getIntExtra(EXTRA_ENTRY_TYPE, ENTRY_TYPE_UNKNOWN); 255 Log.d(LOG_TAG, "Launched from " + entryTypeToString(mEntryType)); 256 257 // Allow this activity to be displayed in front of the keyguard / lockscreen. 258 setShowWhenLocked(true); 259 // Allow turning screen on 260 setTurnScreenOn(true); 261 262 CarrierConfigManager configMgr = getSystemService(CarrierConfigManager.class); 263 PersistableBundle carrierConfig = 264 configMgr.getConfigForSubId(SubscriptionManager.getDefaultVoiceSubscriptionId()); 265 266 mShortcutViewConfig = new ShortcutViewUtils.Config(this, carrierConfig, mEntryType); 267 Log.d(LOG_TAG, "Enable emergency dialer shortcut: " 268 + mShortcutViewConfig.isEnabled()); 269 270 if (mShortcutViewConfig.isEnabled()) { 271 // Shortcut view doesn't support dark text theme. 272 updateTheme(false); 273 } else { 274 WallpaperColors wallpaperColors = 275 getWallpaperManager().getWallpaperColors(WallpaperManager.FLAG_LOCK); 276 updateTheme(supportsDarkText(wallpaperColors)); 277 } 278 279 setContentView(R.layout.emergency_dialer); 280 281 mDigits = (ResizingTextEditText) findViewById(R.id.digits); 282 mDigits.setKeyListener(DialerKeyListener.getInstance()); 283 mDigits.setOnClickListener(this); 284 mDigits.setOnKeyListener(this); 285 mDigits.setLongClickable(false); 286 mDigits.setInputType(InputType.TYPE_NULL); 287 mDefaultDigitsTextSize = mDigits.getScaledTextSize(); 288 maybeAddNumberFormatting(); 289 290 mBackgroundDrawable = new ColorDrawable(); 291 Point displaySize = new Point(); 292 ((WindowManager) getSystemService(Context.WINDOW_SERVICE)) 293 .getDefaultDisplay().getSize(displaySize); 294 mBackgroundDrawable.setAlpha(mShortcutViewConfig.isEnabled() 295 ? BLACK_BACKGROUND_GRADIENT_ALPHA : BACKGROUND_GRADIENT_ALPHA); 296 getWindow().setBackgroundDrawable(mBackgroundDrawable); 297 298 // Check for the presence of the keypad 299 View view = findViewById(R.id.one); 300 if (view != null) { 301 setupKeypad(); 302 } 303 304 mDelete = findViewById(R.id.deleteButton); 305 mDelete.setOnClickListener(this); 306 mDelete.setOnLongClickListener(this); 307 308 mDialButton = findViewById(R.id.floating_action_button); 309 310 // Check whether we should show the onscreen "Dial" button and co. 311 // Read carrier config through the public API because PhoneGlobals is not available when we 312 // run as a secondary user. 313 if (carrierConfig.getBoolean(CarrierConfigManager.KEY_SHOW_ONSCREEN_DIAL_BUTTON_BOOL)) { 314 mDialButton.setOnClickListener(this); 315 } else { 316 mDialButton.setVisibility(View.GONE); 317 } 318 mIsWfcEmergencyCallingWarningEnabled = carrierConfig.getInt( 319 CarrierConfigManager.KEY_EMERGENCY_NOTIFICATION_DELAY_INT) > -1; 320 maybeShowWfcEmergencyCallingWarning(); 321 322 ViewUtil.setupFloatingActionButton(mDialButton, getResources()); 323 324 if (icicle != null) { 325 super.onRestoreInstanceState(icicle); 326 } 327 328 // Extract phone number from intent 329 Uri data = getIntent().getData(); 330 if (data != null && (PhoneAccount.SCHEME_TEL.equals(data.getScheme()))) { 331 String number = PhoneNumberUtils.getNumberFromIntent(getIntent(), this); 332 if (number != null) { 333 mDigits.setText(number); 334 } 335 } 336 337 // if the mToneGenerator creation fails, just continue without it. It is 338 // a local audio signal, and is not as important as the dtmf tone itself. 339 synchronized (mToneGeneratorLock) { 340 if (mToneGenerator == null) { 341 try { 342 mToneGenerator = new ToneGenerator(DIAL_TONE_STREAM_TYPE, TONE_RELATIVE_VOLUME); 343 } catch (RuntimeException e) { 344 Log.w(LOG_TAG, "Exception caught while creating local tone generator: " + e); 345 mToneGenerator = null; 346 } 347 } 348 } 349 350 final IntentFilter intentFilter = new IntentFilter(); 351 intentFilter.addAction(Intent.ACTION_SCREEN_OFF); 352 registerReceiver(mBroadcastReceiver, intentFilter); 353 354 mEmergencyActionGroup = (EmergencyActionGroup) findViewById(R.id.emergency_action_group); 355 356 mEmergencyInfoGroup = (EmergencyInfoGroup) findViewById(R.id.emergency_info_button); 357 358 if (mShortcutViewConfig.isEnabled()) { 359 setupEmergencyShortcutsView(); 360 } 361 } 362 363 @Override onDestroy()364 protected void onDestroy() { 365 super.onDestroy(); 366 synchronized (mToneGeneratorLock) { 367 if (mToneGenerator != null) { 368 mToneGenerator.release(); 369 mToneGenerator = null; 370 } 371 } 372 unregisterReceiver(mBroadcastReceiver); 373 if (mShortcutAdapter != null && mShortcutDataSetObserver != null) { 374 mShortcutAdapter.unregisterDataSetObserver(mShortcutDataSetObserver); 375 mShortcutDataSetObserver = null; 376 } 377 } 378 379 @Override onRestoreInstanceState(Bundle icicle)380 protected void onRestoreInstanceState(Bundle icicle) { 381 mLastNumber = icicle.getString(LAST_NUMBER); 382 } 383 384 @Override onSaveInstanceState(Bundle outState)385 protected void onSaveInstanceState(Bundle outState) { 386 super.onSaveInstanceState(outState); 387 outState.putString(LAST_NUMBER, mLastNumber); 388 } 389 390 /** 391 * Explicitly turn off number formatting, since it gets in the way of the emergency 392 * number detector 393 */ maybeAddNumberFormatting()394 protected void maybeAddNumberFormatting() { 395 // Do nothing. 396 } 397 398 @Override onPostCreate(Bundle savedInstanceState)399 protected void onPostCreate(Bundle savedInstanceState) { 400 super.onPostCreate(savedInstanceState); 401 402 // This can't be done in onCreate(), since the auto-restoring of the digits 403 // will play DTMF tones for all the old digits if it is when onRestoreSavedInstanceState() 404 // is called. This method will be called every time the activity is created, and 405 // will always happen after onRestoreSavedInstanceState(). 406 mDigits.addTextChangedListener(this); 407 } 408 setupKeypad()409 private void setupKeypad() { 410 // Setup the listeners for the buttons 411 for (int id : DIALER_KEYS) { 412 final DialpadKeyButton key = (DialpadKeyButton) findViewById(id); 413 key.setOnPressedListener(this); 414 } 415 416 View view = findViewById(R.id.zero); 417 view.setOnLongClickListener(this); 418 } 419 420 @Override onBackPressed()421 public void onBackPressed() { 422 // If shortcut view is enabled and Dialpad view is visible, pressing the back key will 423 // back to display EmergencyShortcutView view. Otherwise, it would finish the activity. 424 if (mShortcutViewConfig.isEnabled() && mDialpadView != null 425 && mDialpadView.getVisibility() == View.VISIBLE) { 426 switchView(mEmergencyShortcutView, mDialpadView, true); 427 return; 428 } 429 super.onBackPressed(); 430 } 431 432 /** 433 * handle key events 434 */ 435 @Override onKeyDown(int keyCode, KeyEvent event)436 public boolean onKeyDown(int keyCode, KeyEvent event) { 437 switch (keyCode) { 438 // Happen when there's a "Call" hard button. 439 case KeyEvent.KEYCODE_CALL: { 440 if (TextUtils.isEmpty(mDigits.getText().toString())) { 441 // if we are adding a call from the InCallScreen and the phone 442 // number entered is empty, we just close the dialer to expose 443 // the InCallScreen under it. 444 finish(); 445 } else { 446 // otherwise, we place the call. 447 placeCall(); 448 } 449 return true; 450 } 451 } 452 return super.onKeyDown(keyCode, event); 453 } 454 keyPressed(int keyCode)455 private void keyPressed(int keyCode) { 456 mDigits.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); 457 KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode); 458 mDigits.onKeyDown(keyCode, event); 459 } 460 461 @Override onKey(View view, int keyCode, KeyEvent event)462 public boolean onKey(View view, int keyCode, KeyEvent event) { 463 if (view.getId() 464 == R.id.digits) { // Happen when "Done" button of the IME is pressed. This can 465 // happen when this 466 // Activity is forced into landscape mode due to a desk dock. 467 if (keyCode == KeyEvent.KEYCODE_ENTER 468 && event.getAction() == KeyEvent.ACTION_UP) { 469 placeCall(); 470 return true; 471 } 472 } 473 return false; 474 } 475 476 @Override dispatchTouchEvent(MotionEvent ev)477 public boolean dispatchTouchEvent(MotionEvent ev) { 478 onPreTouchEvent(ev); 479 boolean handled = super.dispatchTouchEvent(ev); 480 onPostTouchEvent(ev); 481 return handled; 482 } 483 484 @Override onConfirmClick(EmergencyShortcutButton button)485 public void onConfirmClick(EmergencyShortcutButton button) { 486 if (button == null) return; 487 String phoneNumber = button.getPhoneNumber(); 488 489 if (!TextUtils.isEmpty(phoneNumber)) { 490 if (DBG) Log.d(LOG_TAG, "dial emergency number: " + Rlog.pii(LOG_TAG, phoneNumber)); 491 492 placeCall(phoneNumber, TelecomManager.CALL_SOURCE_EMERGENCY_SHORTCUT, 493 mShortcutViewConfig.getPhoneInfo()); 494 } else { 495 Log.d(LOG_TAG, "emergency number is empty"); 496 } 497 } 498 499 @Override onConfirmClick(EmergencyInfoGroup button)500 public void onConfirmClick(EmergencyInfoGroup button) { 501 if (button == null) return; 502 503 Intent intent = (Intent) button.getTag(R.id.tag_intent); 504 if (intent != null) { 505 startActivity(intent); 506 } 507 } 508 509 @Override onClick(View view)510 public void onClick(View view) { 511 if (view.getId() == R.id.deleteButton) { 512 keyPressed(KeyEvent.KEYCODE_DEL); 513 return; 514 } else if (view.getId() == R.id.floating_action_button) { 515 view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); 516 placeCall(); 517 return; 518 } else if (view.getId() == R.id.digits) { 519 if (mDigits.length() != 0) { 520 mDigits.setCursorVisible(true); 521 } 522 return; 523 } else if (view.getId() == R.id.floating_action_button_dialpad) { 524 mDigits.getText().clear(); 525 switchView(mDialpadView, mEmergencyShortcutView, true); 526 return; 527 } 528 } 529 530 @Override onPressed(View view, boolean pressed)531 public void onPressed(View view, boolean pressed) { 532 if (!pressed) { 533 return; 534 } 535 if (view.getId() == R.id.one) { 536 playTone(ToneGenerator.TONE_DTMF_1); 537 keyPressed(KeyEvent.KEYCODE_1); 538 return; 539 } else if (view.getId() == R.id.two) { 540 playTone(ToneGenerator.TONE_DTMF_2); 541 keyPressed(KeyEvent.KEYCODE_2); 542 return; 543 } else if (view.getId() == R.id.three) { 544 playTone(ToneGenerator.TONE_DTMF_3); 545 keyPressed(KeyEvent.KEYCODE_3); 546 return; 547 } else if (view.getId() == R.id.four) { 548 playTone(ToneGenerator.TONE_DTMF_4); 549 keyPressed(KeyEvent.KEYCODE_4); 550 return; 551 } else if (view.getId() == R.id.five) { 552 playTone(ToneGenerator.TONE_DTMF_5); 553 keyPressed(KeyEvent.KEYCODE_5); 554 return; 555 } else if (view.getId() == R.id.six) { 556 playTone(ToneGenerator.TONE_DTMF_6); 557 keyPressed(KeyEvent.KEYCODE_6); 558 return; 559 } else if (view.getId() == R.id.seven) { 560 playTone(ToneGenerator.TONE_DTMF_7); 561 keyPressed(KeyEvent.KEYCODE_7); 562 return; 563 } else if (view.getId() == R.id.eight) { 564 playTone(ToneGenerator.TONE_DTMF_8); 565 keyPressed(KeyEvent.KEYCODE_8); 566 return; 567 } else if (view.getId() == R.id.nine) { 568 playTone(ToneGenerator.TONE_DTMF_9); 569 keyPressed(KeyEvent.KEYCODE_9); 570 return; 571 } else if (view.getId() == R.id.zero) { 572 playTone(ToneGenerator.TONE_DTMF_0); 573 keyPressed(KeyEvent.KEYCODE_0); 574 return; 575 } else if (view.getId() == R.id.pound) { 576 playTone(ToneGenerator.TONE_DTMF_P); 577 keyPressed(KeyEvent.KEYCODE_POUND); 578 return; 579 } else if (view.getId() == R.id.star) { 580 playTone(ToneGenerator.TONE_DTMF_S); 581 keyPressed(KeyEvent.KEYCODE_STAR); 582 return; 583 } 584 } 585 586 /** 587 * called for long touch events 588 */ 589 @Override onLongClick(View view)590 public boolean onLongClick(View view) { 591 int id = view.getId(); 592 if (id == R.id.deleteButton) { 593 mDigits.getText().clear(); 594 return true; 595 } else if (id == R.id.zero) { 596 removePreviousDigitIfPossible(); 597 keyPressed(KeyEvent.KEYCODE_PLUS); 598 return true; 599 } 600 return false; 601 } 602 603 @Override onStart()604 protected void onStart() { 605 super.onStart(); 606 607 if (mShortcutViewConfig.isEnabled()) { 608 // Shortcut view doesn't support dark text theme. 609 mBackgroundDrawable.setColor(Color.BLACK); 610 updateTheme(false); 611 } else { 612 WallpaperManager wallpaperManager = getWallpaperManager(); 613 if (wallpaperManager.isWallpaperSupported()) { 614 wallpaperManager.addOnColorsChangedListener(this, null); 615 } 616 617 WallpaperColors wallpaperColors = 618 wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_LOCK); 619 mBackgroundDrawable.setColor(getPrimaryColor(wallpaperColors)); 620 updateTheme(supportsDarkText(wallpaperColors)); 621 } 622 623 if (mShortcutViewConfig.isEnabled()) { 624 updateLocationAndEccInfo(); 625 } 626 } 627 628 @Override onResume()629 protected void onResume() { 630 super.onResume(); 631 632 // retrieve the DTMF tone play back setting. 633 mDTMFToneEnabled = Settings.System.getInt(getContentResolver(), 634 Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1; 635 636 // if the mToneGenerator creation fails, just continue without it. It is 637 // a local audio signal, and is not as important as the dtmf tone itself. 638 synchronized (mToneGeneratorLock) { 639 if (mToneGenerator == null) { 640 try { 641 mToneGenerator = new ToneGenerator(AudioManager.STREAM_DTMF, 642 TONE_RELATIVE_VOLUME); 643 } catch (RuntimeException e) { 644 Log.w(LOG_TAG, "Exception caught while creating local tone generator: " + e); 645 mToneGenerator = null; 646 } 647 } 648 } 649 650 updateDialAndDeleteButtonStateEnabledAttr(); 651 } 652 653 @Override onPause()654 public void onPause() { 655 super.onPause(); 656 } 657 658 @Override onStop()659 protected void onStop() { 660 super.onStop(); 661 662 WallpaperManager wallpaperManager = getWallpaperManager(); 663 if (wallpaperManager.isWallpaperSupported()) { 664 wallpaperManager.removeOnColorsChangedListener(this); 665 } 666 } 667 668 /** 669 * Sets theme based on gradient colors 670 * 671 * @param supportsDarkText true if gradient supports dark text 672 */ updateTheme(boolean supportsDarkText)673 private void updateTheme(boolean supportsDarkText) { 674 if (mSupportsDarkText == supportsDarkText) { 675 return; 676 } 677 mSupportsDarkText = supportsDarkText; 678 679 // We can't change themes after inflation, in this case we'll have to recreate 680 // the whole activity. 681 if (mBackgroundDrawable != null) { 682 recreate(); 683 return; 684 } 685 686 int vis = getWindow().getDecorView().getSystemUiVisibility(); 687 if (supportsDarkText) { 688 vis |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; 689 vis |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; 690 setTheme(R.style.EmergencyDialerThemeDark); 691 } else { 692 vis &= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; 693 vis &= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; 694 setTheme(R.style.EmergencyDialerTheme); 695 } 696 getWindow().getDecorView().setSystemUiVisibility(vis); 697 } 698 699 /** 700 * place the call, but check to make sure it is a viable number. 701 */ placeCall()702 private void placeCall() { 703 mLastNumber = mDigits.getText().toString(); 704 705 // Convert into emergency number according to emergency conversion map. 706 // If conversion map is not defined (this is default), this method does 707 // nothing and just returns input number. 708 mLastNumber = PhoneNumberUtils.convertToEmergencyNumber(this, mLastNumber); 709 710 boolean isEmergencyNumber; 711 ShortcutViewUtils.PhoneInfo phoneToMakeCall = null; 712 if (mShortcutAdapter != null && mShortcutAdapter.hasShortcut(mLastNumber)) { 713 isEmergencyNumber = true; 714 phoneToMakeCall = mShortcutViewConfig.getPhoneInfo(); 715 } else if (mShortcutViewConfig.hasPromotedEmergencyNumber(mLastNumber)) { 716 // If a number from SIM/network/... is categoried as police/ambulance/fire, 717 // hasPromotedEmergencyNumber() will return true, but it maybe not promoted as a 718 // shortcut button because a number provided by database has higher priority. 719 isEmergencyNumber = true; 720 phoneToMakeCall = mShortcutViewConfig.getPhoneInfo(); 721 } else { 722 try { 723 isEmergencyNumber = getSystemService(TelephonyManager.class) 724 .isEmergencyNumber(mLastNumber); 725 } catch (IllegalStateException ise) { 726 isEmergencyNumber = false; 727 } 728 } 729 730 if (isEmergencyNumber) { 731 if (DBG) Log.d(LOG_TAG, "placing call to " + mLastNumber); 732 733 // place the call if it is a valid number 734 if (mLastNumber == null || !TextUtils.isGraphic(mLastNumber)) { 735 // There is no number entered. 736 playTone(ToneGenerator.TONE_PROP_NACK); 737 return; 738 } 739 740 placeCall(mLastNumber, TelecomManager.CALL_SOURCE_EMERGENCY_DIALPAD, 741 phoneToMakeCall); 742 } else { 743 if (DBG) Log.d(LOG_TAG, "rejecting bad requested number " + mLastNumber); 744 745 showDialog(BAD_EMERGENCY_NUMBER_DIALOG); 746 } 747 mDigits.getText().delete(0, mDigits.getText().length()); 748 } 749 placeCall(String number, int callSource, ShortcutViewUtils.PhoneInfo phone)750 private void placeCall(String number, int callSource, ShortcutViewUtils.PhoneInfo phone) { 751 Log.d(LOG_TAG, "Place emergency call from " + callSourceToString(callSource) 752 + ", entry = " + entryTypeToString(mEntryType)); 753 754 Bundle extras = new Bundle(); 755 extras.putInt(TelecomManager.EXTRA_CALL_SOURCE, callSource); 756 /** 757 * This is used for Telecom and Telephony to tell modem user's intent is emergency call, 758 * when the dialed number is ambiguous and identified as both emergency number and any 759 * other non-emergency number; e.g. in some situation, 611 could be both an emergency 760 * number in a country and a non-emergency number of a carrier's customer service hotline. 761 */ 762 extras.putBoolean(TelecomManager.EXTRA_IS_USER_INTENT_EMERGENCY_CALL, true); 763 764 if (phone != null && phone.getPhoneAccountHandle() != null) { 765 // Requests to dial through the specified phone. 766 extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, 767 phone.getPhoneAccountHandle()); 768 } 769 770 TelecomManager tm = this.getSystemService(TelecomManager.class); 771 tm.placeCall(Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null), extras); 772 } 773 774 /** 775 * Plays the specified tone for TONE_LENGTH_MS milliseconds. 776 * 777 * The tone is played locally, using the audio stream for phone calls. 778 * Tones are played only if the "Audible touch tones" user preference 779 * is checked, and are NOT played if the device is in silent mode. 780 * 781 * @param tone a tone code from {@link ToneGenerator} 782 */ playTone(int tone)783 void playTone(int tone) { 784 // if local tone playback is disabled, just return. 785 if (!mDTMFToneEnabled) { 786 return; 787 } 788 789 // Also do nothing if the phone is in silent mode. 790 // We need to re-check the ringer mode for *every* playTone() 791 // call, rather than keeping a local flag that's updated in 792 // onResume(), since it's possible to toggle silent mode without 793 // leaving the current activity (via the ENDCALL-longpress menu.) 794 AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 795 int ringerMode = audioManager.getRingerMode(); 796 if ((ringerMode == AudioManager.RINGER_MODE_SILENT) 797 || (ringerMode == AudioManager.RINGER_MODE_VIBRATE)) { 798 return; 799 } 800 801 synchronized (mToneGeneratorLock) { 802 if (mToneGenerator == null) { 803 Log.w(LOG_TAG, "playTone: mToneGenerator == null, tone: " + tone); 804 return; 805 } 806 807 // Start the new tone (will stop any playing tone) 808 mToneGenerator.startTone(tone, TONE_LENGTH_MS); 809 } 810 } 811 createErrorMessage(String number)812 private CharSequence createErrorMessage(String number) { 813 if (!TextUtils.isEmpty(number)) { 814 String errorString = getString(R.string.dial_emergency_error, number); 815 int startingPosition = errorString.indexOf(number); 816 int endingPosition = startingPosition + number.length(); 817 Spannable result = new SpannableString(errorString); 818 PhoneNumberUtils.addTtsSpan(result, startingPosition, endingPosition); 819 return result; 820 } else { 821 return getText(R.string.dial_emergency_empty_error).toString(); 822 } 823 } 824 825 @Override onCreateDialog(int id)826 protected Dialog onCreateDialog(int id) { 827 AlertDialog dialog = null; 828 if (id == BAD_EMERGENCY_NUMBER_DIALOG) { 829 // construct dialog 830 dialog = new AlertDialog.Builder(this, R.style.EmergencyDialerAlertDialogTheme) 831 .setTitle(getText(R.string.emergency_enable_radio_dialog_title)) 832 .setMessage(createErrorMessage(mLastNumber)) 833 .setPositiveButton(R.string.ok, null) 834 .setCancelable(true).create(); 835 836 // blur stuff behind the dialog 837 dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND); 838 setShowWhenLocked(true); 839 } 840 return dialog; 841 } 842 843 @Override onPrepareDialog(int id, Dialog dialog)844 protected void onPrepareDialog(int id, Dialog dialog) { 845 super.onPrepareDialog(id, dialog); 846 if (id == BAD_EMERGENCY_NUMBER_DIALOG) { 847 AlertDialog alert = (AlertDialog) dialog; 848 alert.setMessage(createErrorMessage(mLastNumber)); 849 } 850 } 851 852 @Override onOptionsItemSelected(MenuItem item)853 public boolean onOptionsItemSelected(MenuItem item) { 854 final int itemId = item.getItemId(); 855 if (itemId == android.R.id.home) { 856 onBackPressed(); 857 return true; 858 } 859 return super.onOptionsItemSelected(item); 860 } 861 862 /** 863 * Update the enabledness of the "Dial" and "Backspace" buttons if applicable. 864 */ updateDialAndDeleteButtonStateEnabledAttr()865 private void updateDialAndDeleteButtonStateEnabledAttr() { 866 final boolean notEmpty = mDigits.length() != 0; 867 868 mDelete.setEnabled(notEmpty); 869 } 870 871 /** 872 * Remove the digit just before the current position. Used by various long pressed callbacks 873 * to remove the digit that was populated as a result of the short click. 874 */ removePreviousDigitIfPossible()875 private void removePreviousDigitIfPossible() { 876 final int currentPosition = mDigits.getSelectionStart(); 877 if (currentPosition > 0) { 878 mDigits.setSelection(currentPosition); 879 mDigits.getText().delete(currentPosition - 1, currentPosition); 880 } 881 } 882 883 /** 884 * Update the text-to-speech annotations in the edit field. 885 */ updateTtsSpans()886 private void updateTtsSpans() { 887 for (Object o : mDigits.getText().getSpans(0, mDigits.getText().length(), TtsSpan.class)) { 888 mDigits.getText().removeSpan(o); 889 } 890 PhoneNumberUtils.ttsSpanAsPhoneNumber(mDigits.getText(), 0, mDigits.getText().length()); 891 } 892 893 @Override onColorsChanged(WallpaperColors colors, int which)894 public void onColorsChanged(WallpaperColors colors, int which) { 895 if ((which & WallpaperManager.FLAG_LOCK) != 0) { 896 mBackgroundDrawable.setColor(getPrimaryColor(colors)); 897 updateTheme(supportsDarkText(colors)); 898 } 899 } 900 901 /** 902 * Where a carrier requires a warning that emergency calling is not available while on WFC, 903 * add hint text above the dial pad which warns the user of this case. 904 */ maybeShowWfcEmergencyCallingWarning()905 private void maybeShowWfcEmergencyCallingWarning() { 906 if (!mIsWfcEmergencyCallingWarningEnabled) { 907 Log.i(LOG_TAG, "maybeShowWfcEmergencyCallingWarning: warning disabled by carrier."); 908 return; 909 } 910 911 // Use an async task rather than calling into Telephony on UI thread. 912 AsyncTask<Void, Void, Boolean> showWfcWarningTask = new AsyncTask<Void, Void, Boolean>() { 913 @Override 914 protected Boolean doInBackground(Void... voids) { 915 TelephonyManager tm = getSystemService(TelephonyManager.class); 916 boolean isWfcAvailable = tm.isWifiCallingAvailable(); 917 ServiceState ss = tm.getServiceState(); 918 boolean isCellAvailable = 919 ss.getRilVoiceRadioTechnology() != RIL_RADIO_TECHNOLOGY_UNKNOWN; 920 Log.i(LOG_TAG, "showWfcWarningTask: isWfcAvailable=" + isWfcAvailable 921 + " isCellAvailable=" + isCellAvailable 922 + "(rat=" + ss.getRilVoiceRadioTechnology() + ")"); 923 return isWfcAvailable && !isCellAvailable; 924 } 925 926 @Override 927 protected void onPostExecute(Boolean result) { 928 if (result.booleanValue()) { 929 Log.i(LOG_TAG, "showWfcWarningTask: showing ecall warning"); 930 mDigits.setHint(R.string.dial_emergency_calling_not_available); 931 } else { 932 Log.i(LOG_TAG, "showWfcWarningTask: hiding ecall warning"); 933 mDigits.setHint(""); 934 } 935 maybeChangeHintSize(); 936 } 937 }; 938 showWfcWarningTask.execute((Void) null); 939 } 940 941 /** 942 * Where a hint is applied and there are no digits dialed, disable autoresize of the dial digits 943 * edit view and set the font size to a smaller size appropriate for the emergency calling 944 * warning. 945 */ maybeChangeHintSize()946 private void maybeChangeHintSize() { 947 if (TextUtils.isEmpty(mDigits.getHint()) 948 || !TextUtils.isEmpty(mDigits.getText().toString())) { 949 // No hint or there are dialed digits, so use default size. 950 mDigits.setTextSize(TypedValue.COMPLEX_UNIT_SP, mDefaultDigitsTextSize); 951 // By default, the digits view auto-resizes to fit the text it contains, so 952 // enable that now. 953 mDigits.setResizeEnabled(true); 954 Log.i(LOG_TAG, "no hint - setting to " + mDigits.getScaledTextSize()); 955 } else { 956 // Hint present and no dialed digits, set custom font size appropriate for the warning. 957 mDigits.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimensionPixelSize( 958 R.dimen.emergency_call_warning_size)); 959 // Since we're populating this with a static text string, disable auto-resize. 960 mDigits.setResizeEnabled(false); 961 Log.i(LOG_TAG, "hint - setting to " + mDigits.getScaledTextSize()); 962 } 963 } 964 setupEmergencyShortcutsView()965 private void setupEmergencyShortcutsView() { 966 mEmergencyShortcutView = findViewById(R.id.emergency_dialer_shortcuts); 967 mDialpadView = findViewById(R.id.emergency_dialer); 968 969 mEmergencyShortcutView.setAccessibilityDelegate(mAccessibilityDelegate); 970 mDialpadView.setAccessibilityDelegate(mAccessibilityDelegate); 971 972 final View dialpadButton = findViewById(R.id.floating_action_button_dialpad); 973 dialpadButton.setOnClickListener(this); 974 975 mEmergencyInfoGroup.setOnConfirmClickListener(this); 976 977 mEmergencyShortcutButtonList = new ArrayList<>(); 978 setupEmergencyCallShortcutButton(); 979 980 updateLocationAndEccInfo(); 981 982 switchView(mEmergencyShortcutView, mDialpadView, false); 983 } 984 setLocationInfo()985 private void setLocationInfo() { 986 final View locationInfo = findViewById(R.id.location_info); 987 988 String countryIso = mShortcutViewConfig.getCountryIso(); 989 String countryName = null; 990 if (!TextUtils.isEmpty(countryIso)) { 991 Locale locale = Locale.getDefault(); 992 countryName = new Locale(locale.getLanguage(), countryIso, locale.getVariant()) 993 .getDisplayCountry(); 994 } 995 if (TextUtils.isEmpty(countryName)) { 996 locationInfo.setVisibility(View.INVISIBLE); 997 } else { 998 final TextView location = (TextView) locationInfo.findViewById(R.id.location_text); 999 location.setText(countryName); 1000 locationInfo.setVisibility(View.VISIBLE); 1001 } 1002 } 1003 setupEmergencyCallShortcutButton()1004 private void setupEmergencyCallShortcutButton() { 1005 final ViewGroup shortcutButtonContainer = findViewById( 1006 R.id.emergency_shortcut_buttons_container); 1007 shortcutButtonContainer.setClipToOutline(true); 1008 final TextView emergencyNumberTitle = findViewById(R.id.emergency_number_title); 1009 1010 mShortcutAdapter = new EccShortcutAdapter(this) { 1011 @Override 1012 public View inflateView(View convertView, ViewGroup parent, CharSequence number, 1013 CharSequence description, int iconRes) { 1014 EmergencyShortcutButton button = (EmergencyShortcutButton) getLayoutInflater() 1015 .inflate(R.layout.emergency_shortcut_button, parent, false); 1016 button.setPhoneNumber(number); 1017 button.setPhoneDescription(description); 1018 button.setPhoneTypeIcon(iconRes); 1019 button.setOnConfirmClickListener(EmergencyDialer.this); 1020 return button; 1021 } 1022 }; 1023 mShortcutDataSetObserver = new DataSetObserver() { 1024 @Override 1025 public void onChanged() { 1026 super.onChanged(); 1027 updateLayout(); 1028 } 1029 1030 @Override 1031 public void onInvalidated() { 1032 super.onInvalidated(); 1033 updateLayout(); 1034 } 1035 1036 private void updateLayout() { 1037 // clear previous added buttons 1038 shortcutButtonContainer.removeAllViews(); 1039 mEmergencyShortcutButtonList.clear(); 1040 1041 for (int i = 0; i < mShortcutAdapter.getCount() && i < SHORTCUT_SIZE_LIMIT; ++i) { 1042 EmergencyShortcutButton button = (EmergencyShortcutButton) 1043 mShortcutAdapter.getView(i, null, shortcutButtonContainer); 1044 mEmergencyShortcutButtonList.add(button); 1045 shortcutButtonContainer.addView(button); 1046 } 1047 1048 // Update emergency numbers title for numerous buttons. 1049 if (mEmergencyShortcutButtonList.size() > 1) { 1050 emergencyNumberTitle.setText(getString( 1051 R.string.numerous_emergency_numbers_title)); 1052 } else { 1053 emergencyNumberTitle.setText(getText(R.string.single_emergency_number_title)); 1054 } 1055 } 1056 }; 1057 mShortcutAdapter.registerDataSetObserver(mShortcutDataSetObserver); 1058 } 1059 updateLocationAndEccInfo()1060 private void updateLocationAndEccInfo() { 1061 if (!isFinishing() && !isDestroyed()) { 1062 setLocationInfo(); 1063 if (mShortcutAdapter != null) { 1064 mShortcutAdapter.updateCountryEccInfo(this, mShortcutViewConfig.getPhoneInfo()); 1065 } 1066 } 1067 } 1068 1069 /** 1070 * Called by the activity before a touch event is dispatched to the view hierarchy. 1071 */ onPreTouchEvent(MotionEvent event)1072 private void onPreTouchEvent(MotionEvent event) { 1073 mEmergencyActionGroup.onPreTouchEvent(event); 1074 mEmergencyInfoGroup.onPreTouchEvent(event); 1075 1076 if (mEmergencyShortcutButtonList != null) { 1077 for (EmergencyShortcutButton button : mEmergencyShortcutButtonList) { 1078 button.onPreTouchEvent(event); 1079 } 1080 } 1081 } 1082 1083 /** 1084 * Called by the activity after a touch event is dispatched to the view hierarchy. 1085 */ onPostTouchEvent(MotionEvent event)1086 private void onPostTouchEvent(MotionEvent event) { 1087 mEmergencyActionGroup.onPostTouchEvent(event); 1088 mEmergencyInfoGroup.onPostTouchEvent(event); 1089 1090 if (mEmergencyShortcutButtonList != null) { 1091 for (EmergencyShortcutButton button : mEmergencyShortcutButtonList) { 1092 button.onPostTouchEvent(event); 1093 } 1094 } 1095 } 1096 1097 /** 1098 * Switch two view. 1099 * 1100 * @param displayView the view would be displayed. 1101 * @param hideView the view would be hidden. 1102 * @param hasAnimation is {@code true} when the view should be displayed with animation. 1103 */ switchView(View displayView, View hideView, boolean hasAnimation)1104 private void switchView(View displayView, View hideView, boolean hasAnimation) { 1105 if (displayView == null || hideView == null) { 1106 return; 1107 } 1108 1109 if (displayView.getVisibility() == View.VISIBLE) { 1110 return; 1111 } 1112 1113 if (hasAnimation) { 1114 crossfade(hideView, displayView); 1115 } else { 1116 hideView.setVisibility(View.GONE); 1117 displayView.setVisibility(View.VISIBLE); 1118 } 1119 } 1120 1121 /** 1122 * Fade out and fade in animation between two view transition. 1123 */ crossfade(View fadeOutView, View fadeInView)1124 private void crossfade(View fadeOutView, View fadeInView) { 1125 if (fadeOutView == null || fadeInView == null) { 1126 return; 1127 } 1128 final int shortAnimationDuration = getResources().getInteger( 1129 android.R.integer.config_shortAnimTime); 1130 1131 fadeInView.setAlpha(0f); 1132 fadeInView.setVisibility(View.VISIBLE); 1133 1134 fadeInView.animate() 1135 .alpha(1f) 1136 .setDuration(shortAnimationDuration) 1137 .setListener(null); 1138 1139 fadeOutView.animate() 1140 .alpha(0f) 1141 .setDuration(shortAnimationDuration) 1142 .setListener(new AnimatorListenerAdapter() { 1143 @Override 1144 public void onAnimationEnd(Animator animation) { 1145 fadeOutView.setVisibility(View.GONE); 1146 } 1147 }); 1148 } 1149 isShortcutNumber(String number)1150 private boolean isShortcutNumber(String number) { 1151 if (TextUtils.isEmpty(number) || mEmergencyShortcutButtonList == null) { 1152 return false; 1153 } 1154 1155 boolean isShortcut = false; 1156 for (EmergencyShortcutButton button : mEmergencyShortcutButtonList) { 1157 if (button != null && number.equals(button.getPhoneNumber())) { 1158 isShortcut = true; 1159 break; 1160 } 1161 } 1162 return isShortcut; 1163 } 1164 entryTypeToString(int entryType)1165 private String entryTypeToString(int entryType) { 1166 switch (entryType) { 1167 case ENTRY_TYPE_LOCKSCREEN_BUTTON: 1168 return "LockScreen"; 1169 case ENTRY_TYPE_POWER_MENU: 1170 return "PowerMenu"; 1171 default: 1172 return "Unknown-" + entryType; 1173 } 1174 } 1175 callSourceToString(int callSource)1176 private String callSourceToString(int callSource) { 1177 switch (callSource) { 1178 case TelecomManager.CALL_SOURCE_EMERGENCY_DIALPAD: 1179 return "DialPad"; 1180 case TelecomManager.CALL_SOURCE_EMERGENCY_SHORTCUT: 1181 return "Shortcut"; 1182 default: 1183 return "Unknown-" + callSource; 1184 } 1185 } 1186 getWallpaperManager()1187 private WallpaperManager getWallpaperManager() { 1188 return getSystemService(WallpaperManager.class); 1189 } 1190 supportsDarkText(WallpaperColors colors)1191 private static boolean supportsDarkText(WallpaperColors colors) { 1192 if (colors != null) { 1193 return (colors.getColorHints() & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) != 0; 1194 } 1195 // It's possible that wallpaper colors are null (e.g. when colors are being 1196 // processed or a live wallpaper is used). In this case, fallback to same 1197 // behavior as when shortcut view is enabled. 1198 return false; 1199 } 1200 getPrimaryColor(WallpaperColors colors)1201 private static int getPrimaryColor(WallpaperColors colors) { 1202 if (colors != null) { 1203 return colors.getPrimaryColor().toArgb(); 1204 } 1205 // It's possible that wallpaper colors are null (e.g. when colors are being 1206 // processed or a live wallpaper is used). In this case, fallback to same 1207 // behavior as when shortcut view is enabled. 1208 return Color.BLACK; 1209 } 1210 } 1211