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