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.ToolbarController;
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     private boolean mHasTypeDown;
97 
98     private ToneGenerator mToneGenerator;
99 
100     /**
101      * Creates a new instance of the {@link DialpadFragment} which is used for dialing a number.
102      */
newPlaceCallDialpad()103     public static DialpadFragment newPlaceCallDialpad() {
104         DialpadFragment fragment = newDialpad(MODE_DIAL);
105         return fragment;
106     }
107 
108     /**
109      * Creates a new instance used for emergency dialing.
110      */
newEmergencyDialpad()111     public static DialpadFragment newEmergencyDialpad() {
112         return newDialpad(MODE_EMERGENCY);
113     }
114 
newDialpad(int mode)115     private static DialpadFragment newDialpad(int mode) {
116         DialpadFragment fragment = new DialpadFragment();
117 
118         Bundle args = new Bundle();
119         args.putInt(DIALPAD_MODE_KEY, mode);
120         fragment.setArguments(args);
121         return fragment;
122     }
123 
124     @Override
onCreate(@ullable Bundle savedInstanceState)125     public void onCreate(@Nullable Bundle savedInstanceState) {
126         super.onCreate(savedInstanceState);
127         mMode = getArguments().getInt(DIALPAD_MODE_KEY);
128         L.d(TAG, "onCreate mode: %s", mMode);
129         mToneGenerator = new ToneGenerator(AudioManager.STREAM_DTMF, TONE_RELATIVE_VOLUME);
130 
131         mTypeDownResultsViewModel = ViewModelProviders.of(this).get(
132                 TypeDownResultsViewModel.class);
133         mTypeDownResultsViewModel.getContactSearchResults().observe(this,
134                 contactResults -> mAdapter.setData(contactResults));
135     }
136 
137     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)138     public View onCreateView(LayoutInflater inflater, ViewGroup container,
139             Bundle savedInstanceState) {
140         mHasTypeDown = getResources().getBoolean(R.bool.config_show_type_down_list_on_dialpad);
141         View rootView = inflater.inflate(mHasTypeDown ? R.layout.dialpad_fragment_with_type_down
142                 : R.layout.dialpad_fragment_without_type_down, container, false);
143 
144         mTitleView = rootView.findViewById(R.id.title);
145         mTitleView.setTextAppearance(
146                 mMode == MODE_EMERGENCY ? R.style.TextAppearance_EmergencyDialNumber
147                         : R.style.TextAppearance_DialNumber);
148         mDisplayName = rootView.findViewById(R.id.display_name);
149         mRecyclerView = rootView.findViewById(R.id.list_view);
150         if (mRecyclerView != null) {
151             mRecyclerView.setAdapter(mAdapter);
152         }
153         mLabel = rootView.findViewById(R.id.label);
154         mAvatar = rootView.findViewById(R.id.dialpad_contact_avatar);
155         if (mAvatar != null) {
156             mAvatar.setOutlineProvider(ContactAvatarOutputlineProvider.get());
157         }
158 
159         View callButton = rootView.findViewById(R.id.call_button);
160         callButton.setOnClickListener(v -> {
161             if (!TextUtils.isEmpty(getNumber().toString())) {
162                 UiCallManager.get().placeCall(getNumber().toString());
163                 // Update dialed number UI later in onResume() when in call intent is handled.
164                 getNumber().setLength(0);
165             } else {
166                 setDialedNumber(CallLog.Calls.getLastOutgoingCall(getContext()));
167             }
168         });
169 
170         callButton.addOnUnhandledKeyEventListener((v, event) -> {
171             if (event.getKeyCode() == KeyEvent.KEYCODE_CALL) {
172                 // Use onKeyDown/Up instead of performClick() because it animates the ripple
173                 if (event.getAction() == KeyEvent.ACTION_DOWN) {
174                     callButton.onKeyDown(KeyEvent.KEYCODE_ENTER, event);
175                 } else if (event.getAction() == KeyEvent.ACTION_UP) {
176                     callButton.onKeyUp(KeyEvent.KEYCODE_ENTER, event);
177                 }
178                 return true;
179             } else {
180                 return false;
181             }
182         });
183 
184         mDeleteButton = rootView.findViewById(R.id.delete_button);
185         mDeleteButton.setOnClickListener(v -> removeLastDigit());
186         mDeleteButton.setOnLongClickListener(v -> {
187             clearDialedNumber();
188             return true;
189         });
190 
191         return rootView;
192     }
193 
194     @Override
setupToolbar(ToolbarController toolbar)195     protected void setupToolbar(ToolbarController toolbar) {
196         // Only setup the actionbar if we're in dial mode.
197         // In all the other modes, there will be another fragment in the activity
198         // at the same time, and we don't want to mess up it's action bar.
199         if (mMode == MODE_DIAL) {
200             super.setupToolbar(toolbar);
201         }
202     }
203 
204     @Override
onKeypadKeyLongPressed(@eypadFragment.DialKeyCode int keycode)205     public void onKeypadKeyLongPressed(@KeypadFragment.DialKeyCode int keycode) {
206         switch (keycode) {
207             case KeyEvent.KEYCODE_0:
208                 removeLastDigit();
209                 appendDialedNumber("+");
210                 break;
211             case KeyEvent.KEYCODE_STAR:
212                 removeLastDigit();
213                 appendDialedNumber(",");
214                 break;
215             case KeyEvent.KEYCODE_1:
216                 UiCallManager.get().callVoicemail();
217                 break;
218             default:
219                 break;
220         }
221     }
222 
223     @Override
playTone(int keycode)224     void playTone(int keycode) {
225         L.d(TAG, "start key pressed tone for %s", keycode);
226         mToneGenerator.startTone(mToneMap.get(keycode), TONE_LENGTH_INFINITE);
227     }
228 
229     @Override
stopAllTones()230     void stopAllTones() {
231         L.d(TAG, "stop key pressed tone");
232         mToneGenerator.stopTone();
233     }
234 
235     @Override
presentDialedNumber(@onNull StringBuffer number)236     void presentDialedNumber(@NonNull StringBuffer number) {
237         if (getView() == null) {
238             return;
239         }
240 
241         if (number.length() == 0) {
242             mTitleView.setGravity(Gravity.CENTER);
243             mTitleView.setText(
244                     mMode == MODE_DIAL ? R.string.dial_a_number
245                             : R.string.emergency_call_description);
246             ViewUtils.setVisible(mDeleteButton, false);
247         } else {
248             mTitleView.setGravity(
249                     getResources().getInteger(R.integer.config_dialed_number_gravity));
250             if (number.length() <= MAX_DIAL_NUMBER) {
251                 mTitleView.setText(
252                         TelecomUtils.getFormattedNumber(getContext(), number.toString()));
253             } else {
254                 mTitleView.setText(number.substring(number.length() - MAX_DIAL_NUMBER));
255             }
256             ViewUtils.setVisible(mDeleteButton, true);
257         }
258 
259         if (mHasTypeDown) {
260             resetContactInfo();
261             ViewUtils.setVisible(mRecyclerView, true);
262             mTypeDownResultsViewModel.setSearchQuery(number.toString());
263         } else {
264             presentContactInfo(number.toString());
265         }
266     }
267 
presentContactInfo(@onNull String number)268     private void presentContactInfo(@NonNull String number) {
269         Contact contact = InMemoryPhoneBook.get().lookupContactEntry(number);
270         ViewUtils.setText(mDisplayName, contact == null ? "" : contact.getDisplayName());
271         if (contact != null && getResources().getBoolean(
272                 R.bool.config_show_detailed_user_profile_on_dialpad)) {
273             presentContactDetail(contact, number);
274         } else {
275             resetContactInfo();
276         }
277     }
278 
presentContactDetail(@ullable Contact contact, @NonNull String number)279     private void presentContactDetail(@Nullable Contact contact, @NonNull String number) {
280         PhoneNumber phoneNumber = contact.getPhoneNumber(getContext(), number);
281         CharSequence readableLabel = phoneNumber.getReadableLabel(
282                 getContext().getResources());
283         ViewUtils.setText(mLabel, phoneNumber.isPrimary() ? getContext().getString(
284                 R.string.primary_number_description, readableLabel) : readableLabel);
285         ViewUtils.setVisible(mLabel, true);
286 
287         TelecomUtils.setContactBitmapAsync(getContext(), mAvatar, contact);
288         ViewUtils.setVisible(mAvatar, true);
289     }
290 
resetContactInfo()291     private void resetContactInfo() {
292         ViewUtils.setVisible(mLabel, false);
293         ViewUtils.setVisible(mAvatar, false);
294     }
295 }
296