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