/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.incallui; import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.text.Editable; import android.text.method.DialerKeyListener; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityManager; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.TextView; import com.android.phone.common.dialpad.DialpadKeyButton; import com.android.phone.common.dialpad.DialpadView; import java.util.HashMap; /** * Fragment for call control buttons */ public class DialpadFragment extends BaseFragment implements DialpadPresenter.DialpadUi, View.OnTouchListener, View.OnKeyListener, View.OnHoverListener, View.OnClickListener { private static final int ACCESSIBILITY_DTMF_STOP_DELAY_MILLIS = 50; /** * LinearLayout with getter and setter methods for the translationY property using floats, * for animation purposes. */ public static class DialpadSlidingLinearLayout extends LinearLayout { public DialpadSlidingLinearLayout(Context context) { super(context); } public DialpadSlidingLinearLayout(Context context, AttributeSet attrs) { super(context, attrs); } public DialpadSlidingLinearLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public float getYFraction() { final int height = getHeight(); if (height == 0) return 0; return getTranslationY() / height; } public void setYFraction(float yFraction) { setTranslationY(yFraction * getHeight()); } } /** * LinearLayout that always returns true for onHoverEvent callbacks, to fix * problems with accessibility due to the dialpad overlaying other fragments. */ public static class HoverIgnoringLinearLayout extends LinearLayout { public HoverIgnoringLinearLayout(Context context) { super(context); } public HoverIgnoringLinearLayout(Context context, AttributeSet attrs) { super(context, attrs); } public HoverIgnoringLinearLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public boolean onHoverEvent(MotionEvent event) { return true; } } private EditText mDtmfDialerField; /** Hash Map to map a view id to a character*/ private static final HashMap mDisplayMap = new HashMap(); private static final Handler sHandler = new Handler(Looper.getMainLooper()); /** Set up the static maps*/ static { // Map the buttons to the display characters mDisplayMap.put(R.id.one, '1'); mDisplayMap.put(R.id.two, '2'); mDisplayMap.put(R.id.three, '3'); mDisplayMap.put(R.id.four, '4'); mDisplayMap.put(R.id.five, '5'); mDisplayMap.put(R.id.six, '6'); mDisplayMap.put(R.id.seven, '7'); mDisplayMap.put(R.id.eight, '8'); mDisplayMap.put(R.id.nine, '9'); mDisplayMap.put(R.id.zero, '0'); mDisplayMap.put(R.id.pound, '#'); mDisplayMap.put(R.id.star, '*'); } // KeyListener used with the "dialpad digits" EditText widget. private DTMFKeyListener mDialerKeyListener; private DialpadView mDialpadView; /** * Our own key listener, specialized for dealing with DTMF codes. * 1. Ignore the backspace since it is irrelevant. * 2. Allow ONLY valid DTMF characters to generate a tone and be * sent as a DTMF code. * 3. All other remaining characters are handled by the superclass. * * This code is purely here to handle events from the hardware keyboard * while the DTMF dialpad is up. */ private class DTMFKeyListener extends DialerKeyListener { private DTMFKeyListener() { super(); } /** * Overriden to return correct DTMF-dialable characters. */ @Override protected char[] getAcceptedChars(){ return DTMF_CHARACTERS; } /** special key listener ignores backspace. */ @Override public boolean backspace(View view, Editable content, int keyCode, KeyEvent event) { return false; } /** * Return true if the keyCode is an accepted modifier key for the * dialer (ALT or SHIFT). */ private boolean isAcceptableModifierKey(int keyCode) { switch (keyCode) { case KeyEvent.KEYCODE_ALT_LEFT: case KeyEvent.KEYCODE_ALT_RIGHT: case KeyEvent.KEYCODE_SHIFT_LEFT: case KeyEvent.KEYCODE_SHIFT_RIGHT: return true; default: return false; } } /** * Overriden so that with each valid button press, we start sending * a dtmf code and play a local dtmf tone. */ @Override public boolean onKeyDown(View view, Editable content, int keyCode, KeyEvent event) { // if (DBG) log("DTMFKeyListener.onKeyDown, keyCode " + keyCode + ", view " + view); // find the character char c = (char) lookup(event, content); // if not a long press, and parent onKeyDown accepts the input if (event.getRepeatCount() == 0 && super.onKeyDown(view, content, keyCode, event)) { boolean keyOK = ok(getAcceptedChars(), c); // if the character is a valid dtmf code, start playing the tone and send the // code. if (keyOK) { Log.d(this, "DTMFKeyListener reading '" + c + "' from input."); getPresenter().processDtmf(c); } else { Log.d(this, "DTMFKeyListener rejecting '" + c + "' from input."); } return true; } return false; } /** * Overriden so that with each valid button up, we stop sending * a dtmf code and the dtmf tone. */ @Override public boolean onKeyUp(View view, Editable content, int keyCode, KeyEvent event) { // if (DBG) log("DTMFKeyListener.onKeyUp, keyCode " + keyCode + ", view " + view); super.onKeyUp(view, content, keyCode, event); // find the character char c = (char) lookup(event, content); boolean keyOK = ok(getAcceptedChars(), c); if (keyOK) { Log.d(this, "Stopping the tone for '" + c + "'"); getPresenter().stopDtmf(); return true; } return false; } /** * Handle individual keydown events when we DO NOT have an Editable handy. */ public boolean onKeyDown(KeyEvent event) { char c = lookup(event); Log.d(this, "DTMFKeyListener.onKeyDown: event '" + c + "'"); // if not a long press, and parent onKeyDown accepts the input if (event.getRepeatCount() == 0 && c != 0) { // if the character is a valid dtmf code, start playing the tone and send the // code. if (ok(getAcceptedChars(), c)) { Log.d(this, "DTMFKeyListener reading '" + c + "' from input."); getPresenter().processDtmf(c); return true; } else { Log.d(this, "DTMFKeyListener rejecting '" + c + "' from input."); } } return false; } /** * Handle individual keyup events. * * @param event is the event we are trying to stop. If this is null, * then we just force-stop the last tone without checking if the event * is an acceptable dialer event. */ public boolean onKeyUp(KeyEvent event) { if (event == null) { //the below piece of code sends stopDTMF event unnecessarily even when a null event //is received, hence commenting it. /*if (DBG) log("Stopping the last played tone."); stopTone();*/ return true; } char c = lookup(event); Log.d(this, "DTMFKeyListener.onKeyUp: event '" + c + "'"); // TODO: stopTone does not take in character input, we may want to // consider checking for this ourselves. if (ok(getAcceptedChars(), c)) { Log.d(this, "Stopping the tone for '" + c + "'"); getPresenter().stopDtmf(); return true; } return false; } /** * Find the Dialer Key mapped to this event. * * @return The char value of the input event, otherwise * 0 if no matching character was found. */ private char lookup(KeyEvent event) { // This code is similar to {@link DialerKeyListener#lookup(KeyEvent, Spannable) lookup} int meta = event.getMetaState(); int number = event.getNumber(); if (!((meta & (KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON)) == 0) || (number == 0)) { int match = event.getMatch(getAcceptedChars(), meta); number = (match != 0) ? match : number; } return (char) number; } /** * Check to see if the keyEvent is dialable. */ boolean isKeyEventAcceptable (KeyEvent event) { return (ok(getAcceptedChars(), lookup(event))); } /** * Overrides the characters used in {@link DialerKeyListener#CHARACTERS} * These are the valid dtmf characters. */ public final char[] DTMF_CHARACTERS = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '*' }; } @Override public void onClick(View v) { final AccessibilityManager accessibilityManager = (AccessibilityManager) v.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); // When accessibility is on, simulate press and release to preserve the // semantic meaning of performClick(). Required for Braille support. if (accessibilityManager.isEnabled()) { final int id = v.getId(); // Checking the press state prevents double activation. if (!v.isPressed() && mDisplayMap.containsKey(id)) { getPresenter().processDtmf(mDisplayMap.get(id)); sHandler.postDelayed(new Runnable() { @Override public void run() { getPresenter().stopDtmf(); } }, ACCESSIBILITY_DTMF_STOP_DELAY_MILLIS); } } } @Override public boolean onHover(View v, MotionEvent event) { // When touch exploration is turned on, lifting a finger while inside // the button's hover target bounds should perform a click action. final AccessibilityManager accessibilityManager = (AccessibilityManager) v.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); if (accessibilityManager.isEnabled() && accessibilityManager.isTouchExplorationEnabled()) { final int left = v.getPaddingLeft(); final int right = (v.getWidth() - v.getPaddingRight()); final int top = v.getPaddingTop(); final int bottom = (v.getHeight() - v.getPaddingBottom()); switch (event.getActionMasked()) { case MotionEvent.ACTION_HOVER_ENTER: // Lift-to-type temporarily disables double-tap activation. v.setClickable(false); break; case MotionEvent.ACTION_HOVER_EXIT: final int x = (int) event.getX(); final int y = (int) event.getY(); if ((x > left) && (x < right) && (y > top) && (y < bottom)) { v.performClick(); } v.setClickable(true); break; } } return false; } @Override public boolean onKey(View v, int keyCode, KeyEvent event) { Log.d(this, "onKey: keyCode " + keyCode + ", view " + v); if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { int viewId = v.getId(); if (mDisplayMap.containsKey(viewId)) { switch (event.getAction()) { case KeyEvent.ACTION_DOWN: if (event.getRepeatCount() == 0) { getPresenter().processDtmf(mDisplayMap.get(viewId)); } break; case KeyEvent.ACTION_UP: getPresenter().stopDtmf(); break; } // do not return true [handled] here, since we want the // press / click animation to be handled by the framework. } } return false; } @Override public boolean onTouch(View v, MotionEvent event) { Log.d(this, "onTouch"); int viewId = v.getId(); // if the button is recognized if (mDisplayMap.containsKey(viewId)) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // Append the character mapped to this button, to the display. // start the tone getPresenter().processDtmf(mDisplayMap.get(viewId)); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: // stop the tone on ANY other event, except for MOVE. getPresenter().stopDtmf(); break; } // do not return true [handled] here, since we want the // press / click animation to be handled by the framework. } return false; } // TODO(klp) Adds hardware keyboard listener @Override DialpadPresenter createPresenter() { return new DialpadPresenter(); } @Override DialpadPresenter.DialpadUi getUi() { return this; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View parent = inflater.inflate( com.android.incallui.R.layout.dtmf_twelve_key_dialer_view, container, false); mDialpadView = (DialpadView) parent.findViewById(R.id.dialpad_view); mDialpadView.setCanDigitsBeEdited(false); mDialpadView.setBackgroundResource(R.color.incall_dialpad_background); mDtmfDialerField = (EditText) parent.findViewById(R.id.digits); if (mDtmfDialerField != null) { mDialerKeyListener = new DTMFKeyListener(); mDtmfDialerField.setKeyListener(mDialerKeyListener); // remove the long-press context menus that support // the edit (copy / paste / select) functions. mDtmfDialerField.setLongClickable(false); mDtmfDialerField.setElegantTextHeight(false); configureKeypadListeners(mDialpadView); } return parent; } @Override public void onDestroyView() { mDialerKeyListener = null; super.onDestroyView(); } /** * Getter for Dialpad text. * * @return String containing current Dialpad EditText text. */ public String getDtmfText() { return mDtmfDialerField.getText().toString(); } /** * Sets the Dialpad text field with some text. * * @param text Text to set Dialpad EditText to. */ public void setDtmfText(String text) { mDtmfDialerField.setText(text); } @Override public void setVisible(boolean on) { if (on) { getView().setVisibility(View.VISIBLE); } else { getView().setVisibility(View.INVISIBLE); } } /** * Starts the slide up animation for the Dialpad keys when the Dialpad is revealed. */ public void animateShowDialpad() { final DialpadView dialpadView = (DialpadView) getView().findViewById(R.id.dialpad_view); dialpadView.animateShow(); } @Override public void appendDigitsToField(char digit) { if (mDtmfDialerField != null) { // TODO: maybe *don't* manually append this digit if // mDialpadDigits is focused and this key came from the HW // keyboard, since in that case the EditText field will // get the key event directly and automatically appends // whetever the user types. // (Or, a cleaner fix would be to just make mDialpadDigits // *not* handle HW key presses. That seems to be more // complicated than just setting focusable="false" on it, // though.) mDtmfDialerField.getText().append(digit); } } /** * Called externally (from InCallScreen) to play a DTMF Tone. */ /* package */ boolean onDialerKeyDown(KeyEvent event) { Log.d(this, "Notifying dtmf key down."); if (mDialerKeyListener != null) { return mDialerKeyListener.onKeyDown(event); } else { return false; } } /** * Called externally (from InCallScreen) to cancel the last DTMF Tone played. */ public boolean onDialerKeyUp(KeyEvent event) { Log.d(this, "Notifying dtmf key up."); if (mDialerKeyListener != null) { return mDialerKeyListener.onKeyUp(event); } else { return false; } } private void configureKeypadListeners(View fragmentView) { final int[] buttonIds = new int[] {R.id.zero, R.id.one, R.id.two, R.id.three, R.id.four, R.id.five, R.id.six, R.id.seven, R.id.eight, R.id.nine, R.id.star, R.id.pound}; DialpadKeyButton dialpadKey; for (int i = 0; i < buttonIds.length; i++) { dialpadKey = (DialpadKeyButton) fragmentView.findViewById(buttonIds[i]); dialpadKey.setOnTouchListener(this); dialpadKey.setOnKeyListener(this); dialpadKey.setOnHoverListener(this); dialpadKey.setOnClickListener(this); } } }