blob: 371f5c50a4528f882485f39eac3a57eb688c7399 [file] [log] [blame]
/*
* 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.telephony.PhoneNumberUtils;
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.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<DialpadPresenter, DialpadPresenter.DialpadUi>
implements DialpadPresenter.DialpadUi, View.OnTouchListener, View.OnKeyListener,
View.OnHoverListener, View.OnClickListener {
private static final int ACCESSIBILITY_DTMF_STOP_DELAY_MILLIS = 50;
private final int[] mButtonIds = 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};
/**
* 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());
}
}
private EditText mDtmfDialerField;
/** Hash Map to map a view id to a character*/
private static final HashMap<Integer, Character> mDisplayMap =
new HashMap<Integer, Character>();
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;
private int mCurrentTextColor;
/**
* 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 || keyCode == KeyEvent.KEYCODE_ENTER) {
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
public DialpadPresenter createPresenter() {
return new DialpadPresenter();
}
@Override
public DialpadPresenter.DialpadUi getUi() {
return this;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View parent = inflater.inflate(
R.layout.incall_dialpad_fragment, 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();
}
return parent;
}
@Override
public void onResume() {
super.onResume();
updateColors();
}
public void updateColors() {
int textColor = InCallPresenter.getInstance().getThemeColors().mPrimaryColor;
if (mCurrentTextColor == textColor) {
return;
}
DialpadKeyButton dialpadKey;
for (int i = 0; i < mButtonIds.length; i++) {
dialpadKey = (DialpadKeyButton) mDialpadView.findViewById(mButtonIds[i]);
((TextView) dialpadKey.findViewById(R.id.dialpad_key_number)).setTextColor(textColor);
}
mCurrentTextColor = textColor;
}
@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(PhoneNumberUtils.createTtsSpannable(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() {
DialpadKeyButton dialpadKey;
for (int i = 0; i < mButtonIds.length; i++) {
dialpadKey = (DialpadKeyButton) mDialpadView.findViewById(mButtonIds[i]);
dialpadKey.setOnTouchListener(this);
dialpadKey.setOnKeyListener(this);
dialpadKey.setOnHoverListener(this);
dialpadKey.setOnClickListener(this);
}
}
}