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