1 /* 2 * Copyright (C) 2019 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.car.dialer.ui.dialpad; 18 19 import static com.android.car.dialer.ui.dialpad.DialpadRestrictionViewModel.shouldEnforceNoDialpadRestriction; 20 21 import android.animation.AnimatorInflater; 22 import android.animation.ValueAnimator; 23 import android.os.Bundle; 24 import android.provider.Settings; 25 import android.text.SpannableString; 26 import android.text.Spanned; 27 import android.text.TextUtils; 28 import android.util.SparseArray; 29 import android.view.KeyEvent; 30 import android.view.View; 31 import android.widget.TextView; 32 33 import androidx.annotation.CallSuper; 34 import androidx.annotation.NonNull; 35 import androidx.annotation.Nullable; 36 import androidx.lifecycle.ViewModelProvider; 37 38 import com.android.car.dialer.R; 39 import com.android.car.dialer.log.L; 40 import com.android.car.dialer.ui.common.DialerBaseFragment; 41 import com.android.car.dialer.ui.dialpad.DialpadRestrictionViewModel.DialpadUxrMode; 42 import com.android.car.dialer.ui.view.ScaleSpan; 43 44 /** Fragment that controls the dialpad. */ 45 public abstract class AbstractDialpadFragment extends DialerBaseFragment implements 46 KeypadFragment.KeypadCallback { 47 private static final String TAG = "CD.AbsDialpadFragment"; 48 private static final String DIAL_NUMBER_KEY = "DIAL_NUMBER_KEY"; 49 private static final int PLAY_DTMF_TONE = 1; 50 51 static final SparseArray<Character> sDialValueMap = new SparseArray<>(); 52 53 static { sDialValueMap.put(KeyEvent.KEYCODE_1, B)54 sDialValueMap.put(KeyEvent.KEYCODE_1, '1'); sDialValueMap.put(KeyEvent.KEYCODE_2, B)55 sDialValueMap.put(KeyEvent.KEYCODE_2, '2'); sDialValueMap.put(KeyEvent.KEYCODE_3, B)56 sDialValueMap.put(KeyEvent.KEYCODE_3, '3'); sDialValueMap.put(KeyEvent.KEYCODE_4, B)57 sDialValueMap.put(KeyEvent.KEYCODE_4, '4'); sDialValueMap.put(KeyEvent.KEYCODE_5, B)58 sDialValueMap.put(KeyEvent.KEYCODE_5, '5'); sDialValueMap.put(KeyEvent.KEYCODE_6, B)59 sDialValueMap.put(KeyEvent.KEYCODE_6, '6'); sDialValueMap.put(KeyEvent.KEYCODE_7, B)60 sDialValueMap.put(KeyEvent.KEYCODE_7, '7'); sDialValueMap.put(KeyEvent.KEYCODE_8, B)61 sDialValueMap.put(KeyEvent.KEYCODE_8, '8'); sDialValueMap.put(KeyEvent.KEYCODE_9, B)62 sDialValueMap.put(KeyEvent.KEYCODE_9, '9'); sDialValueMap.put(KeyEvent.KEYCODE_0, B)63 sDialValueMap.put(KeyEvent.KEYCODE_0, '0'); sDialValueMap.put(KeyEvent.KEYCODE_STAR, B)64 sDialValueMap.put(KeyEvent.KEYCODE_STAR, '*'); sDialValueMap.put(KeyEvent.KEYCODE_POUND, B)65 sDialValueMap.put(KeyEvent.KEYCODE_POUND, '#'); 66 } 67 68 private boolean mDTMFToneEnabled; 69 private final StringBuffer mNumber = new StringBuffer(); 70 private ValueAnimator mInputMotionAnimator; 71 private ScaleSpan mScaleSpan; 72 private TextView mTitleView; 73 private int mCurrentlyPlayingTone = KeyEvent.KEYCODE_UNKNOWN; 74 75 private DialpadRestrictionViewModel mDialpadRestrictionViewModel; 76 77 /** Defines how the dialed number should be presented. */ presentDialedNumber( @onNull String number, @NonNull DialpadUxrMode dialpadUxrMode)78 abstract void presentDialedNumber( 79 @NonNull String number, @NonNull DialpadUxrMode dialpadUxrMode); 80 81 /** Plays the tone for the pressed keycode when "play DTMF tone" is enabled in settings. */ playTone(int keycode)82 abstract void playTone(int keycode); 83 84 /** Stops playing all tones when "play DTMF tone" is enabled in settings. */ stopAllTones()85 abstract void stopAllTones(); 86 87 @Override onCreate(Bundle savedInstanceState)88 public void onCreate(Bundle savedInstanceState) { 89 super.onCreate(savedInstanceState); 90 91 if (savedInstanceState != null) { 92 mNumber.append(savedInstanceState.getCharSequence(DIAL_NUMBER_KEY)); 93 } 94 L.d(TAG, "onCreate, number: %s", mNumber); 95 96 if (shouldEnforceNoDialpadRestriction(getActivity())) { 97 mDialpadRestrictionViewModel = 98 new ViewModelProvider(getActivity()).get(DialpadRestrictionViewModel.class); 99 mDialpadRestrictionViewModel.getDialpadMode() 100 .observe(this, this::onDialpadUxrModeChange); 101 } 102 } 103 104 @CallSuper 105 @Override onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)106 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 107 super.onViewCreated(view, savedInstanceState); 108 mTitleView = view.findViewById(R.id.title); 109 if (mTitleView != null && getResources().getBoolean(R.bool.config_enable_dial_motion)) { 110 mInputMotionAnimator = (ValueAnimator) AnimatorInflater.loadAnimator(getContext(), 111 R.animator.scale_down); 112 float startTextSize = mTitleView.getTextSize() * getResources().getFloat( 113 R.integer.config_dial_motion_scale_start); 114 mScaleSpan = new ScaleSpan(startTextSize); 115 } 116 } 117 118 @Override onResume()119 public void onResume() { 120 super.onResume(); 121 mDTMFToneEnabled = Settings.System.getInt(getContext().getContentResolver(), 122 Settings.System.DTMF_TONE_WHEN_DIALING, 1) == PLAY_DTMF_TONE; 123 L.d(TAG, "DTMF tone enabled = %s", String.valueOf(mDTMFToneEnabled)); 124 125 presentDialedNumber(); 126 } 127 128 @Override onPause()129 public void onPause() { 130 super.onPause(); 131 stopAllTones(); 132 } 133 134 @Override onSaveInstanceState(Bundle outState)135 public void onSaveInstanceState(Bundle outState) { 136 super.onSaveInstanceState(outState); 137 outState.putCharSequence(DIAL_NUMBER_KEY, mNumber); 138 } 139 140 @Override onKeypadKeyDown(@eypadFragment.DialKeyCode int keycode)141 public void onKeypadKeyDown(@KeypadFragment.DialKeyCode int keycode) { 142 String digit = sDialValueMap.get(keycode).toString(); 143 appendDialedNumber(digit); 144 145 if (mDTMFToneEnabled) { 146 mCurrentlyPlayingTone = keycode; 147 playTone(keycode); 148 } 149 } 150 151 @Override onKeypadKeyUp(@eypadFragment.DialKeyCode int keycode)152 public void onKeypadKeyUp(@KeypadFragment.DialKeyCode int keycode) { 153 if (mDTMFToneEnabled && keycode == mCurrentlyPlayingTone) { 154 mCurrentlyPlayingTone = KeyEvent.KEYCODE_UNKNOWN; 155 stopAllTones(); 156 } 157 } 158 159 /** Set the dialed number to the given number. Must be called after the fragment is added. */ setDialedNumber(String number)160 public void setDialedNumber(String number) { 161 mNumber.setLength(0); 162 if (!TextUtils.isEmpty(number)) { 163 mNumber.append(number); 164 } 165 presentDialedNumber(); 166 } 167 clearDialedNumber()168 void clearDialedNumber() { 169 mNumber.setLength(0); 170 presentDialedNumber(); 171 } 172 removeLastDigit()173 void removeLastDigit() { 174 if (mNumber.length() != 0) { 175 mNumber.deleteCharAt(mNumber.length() - 1); 176 } 177 presentDialedNumber(); 178 } 179 appendDialedNumber(String number)180 void appendDialedNumber(String number) { 181 mNumber.append(number); 182 presentDialedNumber(); 183 184 if (TextUtils.isEmpty(number)) { 185 return; 186 } 187 188 if (mInputMotionAnimator != null) { 189 final String currentText = mTitleView.getText().toString(); 190 final SpannableString spannableString = new SpannableString(currentText); 191 mInputMotionAnimator.addUpdateListener(valueAnimator -> { 192 float textSize = 193 (float) valueAnimator.getAnimatedValue() * mTitleView.getTextSize(); 194 mScaleSpan.setTextSize(textSize); 195 spannableString.setSpan(mScaleSpan, currentText.length() - number.length(), 196 currentText.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 197 mTitleView.setText(spannableString, TextView.BufferType.SPANNABLE); 198 }); 199 mInputMotionAnimator.start(); 200 } 201 } 202 presentDialedNumber()203 private void presentDialedNumber() { 204 if (mInputMotionAnimator != null) { 205 mInputMotionAnimator.cancel(); 206 mInputMotionAnimator.removeAllUpdateListeners(); 207 } 208 209 if (mDialpadRestrictionViewModel != null) { 210 mDialpadRestrictionViewModel.setCurrentPhoneNumber(mNumber.toString()); 211 } 212 213 presentDialedNumber(mNumber.toString(), getDialpadUxrMode()); 214 } 215 216 @NonNull getNumber()217 StringBuffer getNumber() { 218 return mNumber; 219 } 220 getDialpadUxrMode()221 private DialpadUxrMode getDialpadUxrMode() { 222 return mDialpadRestrictionViewModel == null ? DialpadUxrMode.UNRESTRICTED 223 : mDialpadRestrictionViewModel.getDialpadMode().getValue(); 224 } 225 226 /** 227 * A callback to notify of the dialpad mode change. 228 * 229 * NOTE: the method is never triggered if the app is configured to ignore the "no dialpad" UX 230 * restriction. 231 */ 232 @CallSuper onDialpadUxrModeChange(DialpadUxrMode dialpadUxrMode)233 protected void onDialpadUxrModeChange(DialpadUxrMode dialpadUxrMode) { 234 presentDialedNumber(mNumber.toString(), dialpadUxrMode); 235 } 236 } 237