1 /*
2  * Copyright (C) 2015 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 android.media.AudioManager;
20 import android.media.ToneGenerator;
21 import android.os.Bundle;
22 import android.provider.CallLog;
23 import android.text.TextUtils;
24 import android.view.Gravity;
25 import android.view.KeyEvent;
26 import android.view.LayoutInflater;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import android.widget.ImageButton;
30 import android.widget.ImageView;
31 import android.widget.TextView;
32 
33 import androidx.annotation.NonNull;
34 import androidx.annotation.Nullable;
35 import androidx.lifecycle.ViewModelProviders;
36 
37 import com.android.car.apps.common.util.ViewUtils;
38 import com.android.car.dialer.R;
39 import com.android.car.dialer.log.L;
40 import com.android.car.dialer.telecom.UiCallManager;
41 import com.android.car.dialer.ui.view.ContactAvatarOutputlineProvider;
42 import com.android.car.telephony.common.Contact;
43 import com.android.car.telephony.common.InMemoryPhoneBook;
44 import com.android.car.telephony.common.PhoneNumber;
45 import com.android.car.telephony.common.TelecomUtils;
46 import com.android.car.ui.recyclerview.CarUiRecyclerView;
47 import com.android.car.ui.toolbar.Toolbar;
48 
49 import com.google.common.annotations.VisibleForTesting;
50 import com.google.common.collect.ImmutableMap;
51 
52 /**
53  * Fragment that controls the dialpad.
54  */
55 public class DialpadFragment extends AbstractDialpadFragment {
56     private static final String TAG = "CD.DialpadFragment";
57 
58     private static final String DIALPAD_MODE_KEY = "DIALPAD_MODE_KEY";
59     private static final int MODE_DIAL = 1;
60     private static final int MODE_EMERGENCY = 2;
61 
62     @VisibleForTesting
63     static final int MAX_DIAL_NUMBER = 20;
64 
65     private static final int TONE_RELATIVE_VOLUME = 80;
66     private static final int TONE_LENGTH_INFINITE = -1;
67     private final ImmutableMap<Integer, Integer> mToneMap =
68             ImmutableMap.<Integer, Integer>builder()
69                     .put(KeyEvent.KEYCODE_1, ToneGenerator.TONE_DTMF_1)
70                     .put(KeyEvent.KEYCODE_2, ToneGenerator.TONE_DTMF_2)
71                     .put(KeyEvent.KEYCODE_3, ToneGenerator.TONE_DTMF_3)
72                     .put(KeyEvent.KEYCODE_4, ToneGenerator.TONE_DTMF_4)
73                     .put(KeyEvent.KEYCODE_5, ToneGenerator.TONE_DTMF_5)
74                     .put(KeyEvent.KEYCODE_6, ToneGenerator.TONE_DTMF_6)
75                     .put(KeyEvent.KEYCODE_7, ToneGenerator.TONE_DTMF_7)
76                     .put(KeyEvent.KEYCODE_8, ToneGenerator.TONE_DTMF_8)
77                     .put(KeyEvent.KEYCODE_9, ToneGenerator.TONE_DTMF_9)
78                     .put(KeyEvent.KEYCODE_0, ToneGenerator.TONE_DTMF_0)
79                     .put(KeyEvent.KEYCODE_STAR, ToneGenerator.TONE_DTMF_S)
80                     .put(KeyEvent.KEYCODE_POUND, ToneGenerator.TONE_DTMF_P)
81                     .build();
82     private final TypeDownResultsAdapter mAdapter = new TypeDownResultsAdapter();
83 
84     private TypeDownResultsViewModel mTypeDownResultsViewModel;
85     private TextView mTitleView;
86     @Nullable
87     private TextView mDisplayName;
88     @Nullable
89     private CarUiRecyclerView mRecyclerView;
90     @Nullable
91     private TextView mLabel;
92     @Nullable
93     private ImageView mAvatar;
94     private ImageButton mDeleteButton;
95     private int mMode;
96 
97     private ToneGenerator mToneGenerator;
98 
99     /**
100      * Creates a new instance of the {@link DialpadFragment} which is used for dialing a number.
101      */
newPlaceCallDialpad()102     public static DialpadFragment newPlaceCallDialpad() {
103         DialpadFragment fragment = newDialpad(MODE_DIAL);
104         return fragment;
105     }
106 
107     /**
108      * Creates a new instance used for emergency dialing.
109      */
newEmergencyDialpad()110     public static DialpadFragment newEmergencyDialpad() {
111         return newDialpad(MODE_EMERGENCY);
112     }
113 
newDialpad(int mode)114     private static DialpadFragment newDialpad(int mode) {
115         DialpadFragment fragment = new DialpadFragment();
116 
117         Bundle args = new Bundle();
118         args.putInt(DIALPAD_MODE_KEY, mode);
119         fragment.setArguments(args);
120         return fragment;
121     }
122 
123     @Override
onCreate(@ullable Bundle savedInstanceState)124     public void onCreate(@Nullable Bundle savedInstanceState) {
125         super.onCreate(savedInstanceState);
126         mMode = getArguments().getInt(DIALPAD_MODE_KEY);
127         L.d(TAG, "onCreate mode: %s", mMode);
128         mToneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, TONE_RELATIVE_VOLUME);
129 
130         mTypeDownResultsViewModel = ViewModelProviders.of(this).get(
131                 TypeDownResultsViewModel.class);
132         mTypeDownResultsViewModel.getContactSearchResults().observe(this,
133                 contactResults -> mAdapter.setData(contactResults));
134     }
135 
136     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)137     public View onCreateView(LayoutInflater inflater, ViewGroup container,
138             Bundle savedInstanceState) {
139         View rootView = inflater.inflate(R.layout.dialpad_fragment, container, false);
140 
141         mTitleView = rootView.findViewById(R.id.title);
142         mTitleView.setTextAppearance(
143                 mMode == MODE_EMERGENCY ? R.style.TextAppearance_EmergencyDialNumber
144                         : R.style.TextAppearance_DialNumber);
145         mDisplayName = rootView.findViewById(R.id.display_name);
146         mRecyclerView = rootView.findViewById(R.id.list_view);
147         mRecyclerView.setAdapter(mAdapter);
148         mLabel = rootView.findViewById(R.id.label);
149         mAvatar = rootView.findViewById(R.id.dialpad_contact_avatar);
150         if (mAvatar != null) {
151             mAvatar.setOutlineProvider(ContactAvatarOutputlineProvider.get());
152         }
153 
154         View callButton = rootView.findViewById(R.id.call_button);
155         callButton.setOnClickListener(v -> {
156             if (!TextUtils.isEmpty(getNumber().toString())) {
157                 UiCallManager.get().placeCall(getNumber().toString());
158                 // Update dialed number UI later in onResume() when in call intent is handled.
159                 getNumber().setLength(0);
160             } else {
161                 setDialedNumber(CallLog.Calls.getLastOutgoingCall(getContext()));
162             }
163         });
164 
165         callButton.addOnUnhandledKeyEventListener((v, event) -> {
166             if (event.getKeyCode() == KeyEvent.KEYCODE_CALL) {
167                 // Use onKeyDown/Up instead of performClick() because it animates the ripple
168                 if (event.getAction() == KeyEvent.ACTION_DOWN) {
169                     callButton.onKeyDown(KeyEvent.KEYCODE_ENTER, event);
170                 } else if (event.getAction() == KeyEvent.ACTION_UP) {
171                     callButton.onKeyUp(KeyEvent.KEYCODE_ENTER, event);
172                 }
173                 return true;
174             } else {
175                 return false;
176             }
177         });
178 
179         mDeleteButton = rootView.findViewById(R.id.delete_button);
180         mDeleteButton.setOnClickListener(v -> removeLastDigit());
181         mDeleteButton.setOnLongClickListener(v -> {
182             clearDialedNumber();
183             return true;
184         });
185 
186         return rootView;
187     }
188 
189     @Override
setupToolbar(Toolbar toolbar)190     protected void setupToolbar(Toolbar toolbar) {
191         // Only setup the actionbar if we're in dial mode.
192         // In all the other modes, there will be another fragment in the activity
193         // at the same time, and we don't want to mess up it's action bar.
194         if (mMode == MODE_DIAL) {
195             super.setupToolbar(toolbar);
196         }
197     }
198 
199     @Override
onKeypadKeyLongPressed(@eypadFragment.DialKeyCode int keycode)200     public void onKeypadKeyLongPressed(@KeypadFragment.DialKeyCode int keycode) {
201         switch (keycode) {
202             case KeyEvent.KEYCODE_0:
203                 removeLastDigit();
204                 appendDialedNumber("+");
205                 break;
206             case KeyEvent.KEYCODE_STAR:
207                 removeLastDigit();
208                 appendDialedNumber(",");
209                 break;
210             case KeyEvent.KEYCODE_1:
211                 UiCallManager.get().callVoicemail();
212                 break;
213             default:
214                 break;
215         }
216     }
217 
218     @Override
playTone(int keycode)219     void playTone(int keycode) {
220         L.d(TAG, "start key pressed tone for %s", keycode);
221         mToneGenerator.startTone(mToneMap.get(keycode), TONE_LENGTH_INFINITE);
222     }
223 
224     @Override
stopAllTones()225     void stopAllTones() {
226         L.d(TAG, "stop key pressed tone");
227         mToneGenerator.stopTone();
228     }
229 
230     @Override
presentDialedNumber(@onNull StringBuffer number)231     void presentDialedNumber(@NonNull StringBuffer number) {
232         if (getView() == null) {
233             return;
234         }
235 
236         if (number.length() == 0) {
237             mTitleView.setGravity(Gravity.CENTER);
238             mTitleView.setText(
239                     mMode == MODE_DIAL ? R.string.dial_a_number
240                             : R.string.emergency_call_description);
241             ViewUtils.setVisible(mDeleteButton, false);
242         } else {
243             mTitleView.setGravity(
244                     getResources().getInteger(R.integer.config_dialed_number_gravity));
245             if (number.length() <= MAX_DIAL_NUMBER) {
246                 mTitleView.setText(
247                         TelecomUtils.getFormattedNumber(getContext(), number.toString()));
248             } else {
249                 mTitleView.setText(number.substring(number.length() - MAX_DIAL_NUMBER));
250             }
251             ViewUtils.setVisible(mDeleteButton, true);
252         }
253 
254         if (getResources().getBoolean(R.bool.config_show_type_down_list_on_dialpad)) {
255             resetContactInfo();
256             ViewUtils.setVisible(mRecyclerView, true);
257             mTypeDownResultsViewModel.setSearchQuery(number.toString());
258         } else {
259             presentContactInfo(number.toString());
260         }
261     }
262 
263     @Override
onToolbarHeightChange(int toolbarHeight)264     public void onToolbarHeightChange(int toolbarHeight) {
265         // Offset the dialpad to under the tabs in normal dial mode.
266         getView().setPadding(0, mMode == MODE_DIAL ? toolbarHeight : 0, 0, 0);
267     }
268 
presentContactInfo(@onNull String number)269     private void presentContactInfo(@NonNull String number) {
270         Contact contact = InMemoryPhoneBook.get().lookupContactEntry(number);
271         ViewUtils.setText(mDisplayName, contact == null ? "" : contact.getDisplayName());
272         if (contact != null && getResources().getBoolean(
273                 R.bool.config_show_detailed_user_profile_on_dialpad)) {
274             presentContactDetail(contact, number);
275         } else {
276             resetContactInfo();
277         }
278     }
279 
presentContactDetail(@ullable Contact contact, @NonNull String number)280     private void presentContactDetail(@Nullable Contact contact, @NonNull String number) {
281         PhoneNumber phoneNumber = contact.getPhoneNumber(getContext(), number);
282         CharSequence readableLabel = phoneNumber.getReadableLabel(
283                 getContext().getResources());
284         ViewUtils.setText(mLabel, phoneNumber.isPrimary() ? getContext().getString(
285                 R.string.primary_number_description, readableLabel) : readableLabel);
286         ViewUtils.setVisible(mLabel, true);
287 
288         TelecomUtils.setContactBitmapAsync(getContext(), mAvatar, contact);
289         ViewUtils.setVisible(mAvatar, true);
290     }
291 
resetContactInfo()292     private void resetContactInfo() {
293         ViewUtils.setVisible(mLabel, false);
294         ViewUtils.setVisible(mAvatar, false);
295     }
296 }
297