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