1 /* 2 * Copyright (C) 2013 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.incallui; 18 19 import android.content.Context; 20 import android.os.Bundle; 21 import android.text.Editable; 22 import android.text.method.DialerKeyListener; 23 import android.util.ArrayMap; 24 import android.util.AttributeSet; 25 import android.view.KeyEvent; 26 import android.view.LayoutInflater; 27 import android.view.View; 28 import android.view.View.OnClickListener; 29 import android.view.View.OnKeyListener; 30 import android.view.ViewGroup; 31 import android.widget.EditText; 32 import android.widget.LinearLayout; 33 import android.widget.TextView; 34 import com.android.contacts.common.compat.PhoneNumberUtilsCompat; 35 import com.android.dialer.dialpadview.DialpadKeyButton; 36 import com.android.dialer.dialpadview.DialpadKeyButton.OnPressedListener; 37 import com.android.dialer.dialpadview.DialpadView; 38 import com.android.incallui.DialpadPresenter.DialpadUi; 39 import com.android.incallui.baseui.BaseFragment; 40 import java.util.Map; 41 42 /** Fragment for call control buttons */ 43 public class DialpadFragment extends BaseFragment<DialpadPresenter, DialpadUi> 44 implements DialpadUi, OnKeyListener, OnClickListener, OnPressedListener { 45 46 /** Hash Map to map a view id to a character */ 47 private static final Map<Integer, Character> mDisplayMap = new ArrayMap<>(); 48 49 /** Set up the static maps */ 50 static { 51 // Map the buttons to the display characters mDisplayMap.put(R.id.one, '1')52 mDisplayMap.put(R.id.one, '1'); mDisplayMap.put(R.id.two, '2')53 mDisplayMap.put(R.id.two, '2'); mDisplayMap.put(R.id.three, '3')54 mDisplayMap.put(R.id.three, '3'); mDisplayMap.put(R.id.four, '4')55 mDisplayMap.put(R.id.four, '4'); mDisplayMap.put(R.id.five, '5')56 mDisplayMap.put(R.id.five, '5'); mDisplayMap.put(R.id.six, '6')57 mDisplayMap.put(R.id.six, '6'); mDisplayMap.put(R.id.seven, '7')58 mDisplayMap.put(R.id.seven, '7'); mDisplayMap.put(R.id.eight, '8')59 mDisplayMap.put(R.id.eight, '8'); mDisplayMap.put(R.id.nine, '9')60 mDisplayMap.put(R.id.nine, '9'); mDisplayMap.put(R.id.zero, '0')61 mDisplayMap.put(R.id.zero, '0'); mDisplayMap.put(R.id.pound, '#')62 mDisplayMap.put(R.id.pound, '#'); mDisplayMap.put(R.id.star, '*')63 mDisplayMap.put(R.id.star, '*'); 64 } 65 66 private final int[] mButtonIds = 67 new int[] { 68 R.id.zero, 69 R.id.one, 70 R.id.two, 71 R.id.three, 72 R.id.four, 73 R.id.five, 74 R.id.six, 75 R.id.seven, 76 R.id.eight, 77 R.id.nine, 78 R.id.star, 79 R.id.pound 80 }; 81 private EditText mDtmfDialerField; 82 // KeyListener used with the "dialpad digits" EditText widget. 83 private DTMFKeyListener mDialerKeyListener; 84 private DialpadView mDialpadView; 85 private int mCurrentTextColor; 86 87 @Override onClick(View v)88 public void onClick(View v) { 89 if (v.getId() == R.id.dialpad_back) { 90 getActivity().onBackPressed(); 91 } 92 } 93 94 @Override onKey(View v, int keyCode, KeyEvent event)95 public boolean onKey(View v, int keyCode, KeyEvent event) { 96 Log.d(this, "onKey: keyCode " + keyCode + ", view " + v); 97 98 if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) { 99 int viewId = v.getId(); 100 if (mDisplayMap.containsKey(viewId)) { 101 switch (event.getAction()) { 102 case KeyEvent.ACTION_DOWN: 103 if (event.getRepeatCount() == 0) { 104 getPresenter().processDtmf(mDisplayMap.get(viewId)); 105 } 106 break; 107 case KeyEvent.ACTION_UP: 108 getPresenter().stopDtmf(); 109 break; 110 } 111 // do not return true [handled] here, since we want the 112 // press / click animation to be handled by the framework. 113 } 114 } 115 return false; 116 } 117 118 @Override createPresenter()119 public DialpadPresenter createPresenter() { 120 return new DialpadPresenter(); 121 } 122 123 @Override getUi()124 public DialpadPresenter.DialpadUi getUi() { 125 return this; 126 } 127 128 // TODO Adds hardware keyboard listener 129 130 @Override onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)131 public View onCreateView( 132 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 133 final View parent = inflater.inflate(R.layout.incall_dialpad_fragment, container, false); 134 mDialpadView = (DialpadView) parent.findViewById(R.id.dialpad_view); 135 mDialpadView.setCanDigitsBeEdited(false); 136 mDialpadView.setBackgroundResource(R.color.incall_dialpad_background); 137 mDtmfDialerField = (EditText) parent.findViewById(R.id.digits); 138 if (mDtmfDialerField != null) { 139 mDialerKeyListener = new DTMFKeyListener(); 140 mDtmfDialerField.setKeyListener(mDialerKeyListener); 141 // remove the long-press context menus that support 142 // the edit (copy / paste / select) functions. 143 mDtmfDialerField.setLongClickable(false); 144 mDtmfDialerField.setElegantTextHeight(false); 145 configureKeypadListeners(); 146 } 147 View backButton = mDialpadView.findViewById(R.id.dialpad_back); 148 backButton.setVisibility(View.VISIBLE); 149 backButton.setOnClickListener(this); 150 151 return parent; 152 } 153 154 @Override onResume()155 public void onResume() { 156 super.onResume(); 157 updateColors(); 158 } 159 updateColors()160 public void updateColors() { 161 int textColor = InCallPresenter.getInstance().getThemeColorManager().getPrimaryColor(); 162 163 if (mCurrentTextColor == textColor) { 164 return; 165 } 166 167 DialpadKeyButton dialpadKey; 168 for (int i = 0; i < mButtonIds.length; i++) { 169 dialpadKey = (DialpadKeyButton) mDialpadView.findViewById(mButtonIds[i]); 170 ((TextView) dialpadKey.findViewById(R.id.dialpad_key_number)).setTextColor(textColor); 171 } 172 173 mCurrentTextColor = textColor; 174 } 175 176 @Override onDestroyView()177 public void onDestroyView() { 178 mDialerKeyListener = null; 179 super.onDestroyView(); 180 } 181 182 /** 183 * Getter for Dialpad text. 184 * 185 * @return String containing current Dialpad EditText text. 186 */ getDtmfText()187 public String getDtmfText() { 188 return mDtmfDialerField.getText().toString(); 189 } 190 191 /** 192 * Sets the Dialpad text field with some text. 193 * 194 * @param text Text to set Dialpad EditText to. 195 */ setDtmfText(String text)196 public void setDtmfText(String text) { 197 mDtmfDialerField.setText(PhoneNumberUtilsCompat.createTtsSpannable(text)); 198 } 199 200 @Override setVisible(boolean on)201 public void setVisible(boolean on) { 202 if (on) { 203 getView().setVisibility(View.VISIBLE); 204 } else { 205 getView().setVisibility(View.INVISIBLE); 206 } 207 } 208 209 /** Starts the slide up animation for the Dialpad keys when the Dialpad is revealed. */ animateShowDialpad()210 public void animateShowDialpad() { 211 final DialpadView dialpadView = (DialpadView) getView().findViewById(R.id.dialpad_view); 212 dialpadView.animateShow(); 213 } 214 215 @Override appendDigitsToField(char digit)216 public void appendDigitsToField(char digit) { 217 if (mDtmfDialerField != null) { 218 // TODO: maybe *don't* manually append this digit if 219 // mDialpadDigits is focused and this key came from the HW 220 // keyboard, since in that case the EditText field will 221 // get the key event directly and automatically appends 222 // whetever the user types. 223 // (Or, a cleaner fix would be to just make mDialpadDigits 224 // *not* handle HW key presses. That seems to be more 225 // complicated than just setting focusable="false" on it, 226 // though.) 227 mDtmfDialerField.getText().append(digit); 228 } 229 } 230 231 /** Called externally (from InCallScreen) to play a DTMF Tone. */ onDialerKeyDown(KeyEvent event)232 /* package */ boolean onDialerKeyDown(KeyEvent event) { 233 Log.d(this, "Notifying dtmf key down."); 234 if (mDialerKeyListener != null) { 235 return mDialerKeyListener.onKeyDown(event); 236 } else { 237 return false; 238 } 239 } 240 241 /** Called externally (from InCallScreen) to cancel the last DTMF Tone played. */ onDialerKeyUp(KeyEvent event)242 public boolean onDialerKeyUp(KeyEvent event) { 243 Log.d(this, "Notifying dtmf key up."); 244 if (mDialerKeyListener != null) { 245 return mDialerKeyListener.onKeyUp(event); 246 } else { 247 return false; 248 } 249 } 250 configureKeypadListeners()251 private void configureKeypadListeners() { 252 DialpadKeyButton dialpadKey; 253 for (int i = 0; i < mButtonIds.length; i++) { 254 dialpadKey = (DialpadKeyButton) mDialpadView.findViewById(mButtonIds[i]); 255 dialpadKey.setOnKeyListener(this); 256 dialpadKey.setOnClickListener(this); 257 dialpadKey.setOnPressedListener(this); 258 } 259 } 260 261 @Override onPressed(View view, boolean pressed)262 public void onPressed(View view, boolean pressed) { 263 if (pressed && mDisplayMap.containsKey(view.getId())) { 264 Log.d(this, "onPressed: " + pressed + " " + mDisplayMap.get(view.getId())); 265 getPresenter().processDtmf(mDisplayMap.get(view.getId())); 266 } 267 if (!pressed) { 268 Log.d(this, "onPressed: " + pressed); 269 getPresenter().stopDtmf(); 270 } 271 } 272 273 /** 274 * LinearLayout with getter and setter methods for the translationY property using floats, for 275 * animation purposes. 276 */ 277 public static class DialpadSlidingLinearLayout extends LinearLayout { 278 DialpadSlidingLinearLayout(Context context)279 public DialpadSlidingLinearLayout(Context context) { 280 super(context); 281 } 282 DialpadSlidingLinearLayout(Context context, AttributeSet attrs)283 public DialpadSlidingLinearLayout(Context context, AttributeSet attrs) { 284 super(context, attrs); 285 } 286 DialpadSlidingLinearLayout(Context context, AttributeSet attrs, int defStyle)287 public DialpadSlidingLinearLayout(Context context, AttributeSet attrs, int defStyle) { 288 super(context, attrs, defStyle); 289 } 290 getYFraction()291 public float getYFraction() { 292 final int height = getHeight(); 293 if (height == 0) { 294 return 0; 295 } 296 return getTranslationY() / height; 297 } 298 setYFraction(float yFraction)299 public void setYFraction(float yFraction) { 300 setTranslationY(yFraction * getHeight()); 301 } 302 } 303 304 /** 305 * Our own key listener, specialized for dealing with DTMF codes. 1. Ignore the backspace since it 306 * is irrelevant. 2. Allow ONLY valid DTMF characters to generate a tone and be sent as a DTMF 307 * code. 3. All other remaining characters are handled by the superclass. 308 * 309 * <p>This code is purely here to handle events from the hardware keyboard while the DTMF dialpad 310 * is up. 311 */ 312 private class DTMFKeyListener extends DialerKeyListener { 313 314 /** 315 * Overrides the characters used in {@link DialerKeyListener#CHARACTERS} These are the valid 316 * dtmf characters. 317 */ 318 public final char[] DTMF_CHARACTERS = 319 new char[] {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '*'}; 320 DTMFKeyListener()321 private DTMFKeyListener() { 322 super(); 323 } 324 325 /** Overriden to return correct DTMF-dialable characters. */ 326 @Override getAcceptedChars()327 protected char[] getAcceptedChars() { 328 return DTMF_CHARACTERS; 329 } 330 331 /** special key listener ignores backspace. */ 332 @Override backspace(View view, Editable content, int keyCode, KeyEvent event)333 public boolean backspace(View view, Editable content, int keyCode, KeyEvent event) { 334 return false; 335 } 336 337 /** 338 * Overriden so that with each valid button press, we start sending a dtmf code and play a local 339 * dtmf tone. 340 */ 341 @Override onKeyDown(View view, Editable content, int keyCode, KeyEvent event)342 public boolean onKeyDown(View view, Editable content, int keyCode, KeyEvent event) { 343 // if (DBG) log("DTMFKeyListener.onKeyDown, keyCode " + keyCode + ", view " + view); 344 345 // find the character 346 char c = (char) lookup(event, content); 347 348 // if not a long press, and parent onKeyDown accepts the input 349 if (event.getRepeatCount() == 0 && super.onKeyDown(view, content, keyCode, event)) { 350 351 boolean keyOK = ok(getAcceptedChars(), c); 352 353 // if the character is a valid dtmf code, start playing the tone and send the 354 // code. 355 if (keyOK) { 356 Log.d(this, "DTMFKeyListener reading '" + c + "' from input."); 357 getPresenter().processDtmf(c); 358 } else { 359 Log.d(this, "DTMFKeyListener rejecting '" + c + "' from input."); 360 } 361 return true; 362 } 363 return false; 364 } 365 366 /** 367 * Overriden so that with each valid button up, we stop sending a dtmf code and the dtmf tone. 368 */ 369 @Override onKeyUp(View view, Editable content, int keyCode, KeyEvent event)370 public boolean onKeyUp(View view, Editable content, int keyCode, KeyEvent event) { 371 // if (DBG) log("DTMFKeyListener.onKeyUp, keyCode " + keyCode + ", view " + view); 372 373 super.onKeyUp(view, content, keyCode, event); 374 375 // find the character 376 char c = (char) lookup(event, content); 377 378 boolean keyOK = ok(getAcceptedChars(), c); 379 380 if (keyOK) { 381 Log.d(this, "Stopping the tone for '" + c + "'"); 382 getPresenter().stopDtmf(); 383 return true; 384 } 385 386 return false; 387 } 388 389 /** Handle individual keydown events when we DO NOT have an Editable handy. */ onKeyDown(KeyEvent event)390 public boolean onKeyDown(KeyEvent event) { 391 char c = lookup(event); 392 Log.d(this, "DTMFKeyListener.onKeyDown: event '" + c + "'"); 393 394 // if not a long press, and parent onKeyDown accepts the input 395 if (event.getRepeatCount() == 0 && c != 0) { 396 // if the character is a valid dtmf code, start playing the tone and send the 397 // code. 398 if (ok(getAcceptedChars(), c)) { 399 Log.d(this, "DTMFKeyListener reading '" + c + "' from input."); 400 getPresenter().processDtmf(c); 401 return true; 402 } else { 403 Log.d(this, "DTMFKeyListener rejecting '" + c + "' from input."); 404 } 405 } 406 return false; 407 } 408 409 /** 410 * Handle individual keyup events. 411 * 412 * @param event is the event we are trying to stop. If this is null, then we just force-stop the 413 * last tone without checking if the event is an acceptable dialer event. 414 */ onKeyUp(KeyEvent event)415 public boolean onKeyUp(KeyEvent event) { 416 if (event == null) { 417 //the below piece of code sends stopDTMF event unnecessarily even when a null event 418 //is received, hence commenting it. 419 /*if (DBG) log("Stopping the last played tone."); 420 stopTone();*/ 421 return true; 422 } 423 424 char c = lookup(event); 425 Log.d(this, "DTMFKeyListener.onKeyUp: event '" + c + "'"); 426 427 // TODO: stopTone does not take in character input, we may want to 428 // consider checking for this ourselves. 429 if (ok(getAcceptedChars(), c)) { 430 Log.d(this, "Stopping the tone for '" + c + "'"); 431 getPresenter().stopDtmf(); 432 return true; 433 } 434 435 return false; 436 } 437 438 /** 439 * Find the Dialer Key mapped to this event. 440 * 441 * @return The char value of the input event, otherwise 0 if no matching character was found. 442 */ lookup(KeyEvent event)443 private char lookup(KeyEvent event) { 444 // This code is similar to {@link DialerKeyListener#lookup(KeyEvent, Spannable) lookup} 445 int meta = event.getMetaState(); 446 int number = event.getNumber(); 447 448 if (!((meta & (KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON)) == 0) || (number == 0)) { 449 int match = event.getMatch(getAcceptedChars(), meta); 450 number = (match != 0) ? match : number; 451 } 452 453 return (char) number; 454 } 455 456 /** Check to see if the keyEvent is dialable. */ isKeyEventAcceptable(KeyEvent event)457 boolean isKeyEventAcceptable(KeyEvent event) { 458 return (ok(getAcceptedChars(), lookup(event))); 459 } 460 } 461 } 462