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