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