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