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