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