Home | History | Annotate | Download | only in incallui
      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.content.res.Resources;
     21 import android.os.Bundle;
     22 import android.text.Editable;
     23 import android.text.method.DialerKeyListener;
     24 import android.util.AttributeSet;
     25 import android.view.KeyEvent;
     26 import android.view.LayoutInflater;
     27 import android.view.MotionEvent;
     28 import android.view.View;
     29 import android.view.ViewGroup;
     30 import android.view.ViewTreeObserver;
     31 import android.view.accessibility.AccessibilityManager;
     32 import android.widget.EditText;
     33 import android.widget.LinearLayout;
     34 import android.widget.TableRow;
     35 import android.widget.TextView;
     36 
     37 import java.util.HashMap;
     38 
     39 /**
     40  * Fragment for call control buttons
     41  */
     42 public class DialpadFragment extends BaseFragment<DialpadPresenter, DialpadPresenter.DialpadUi>
     43         implements DialpadPresenter.DialpadUi, View.OnTouchListener, View.OnKeyListener,
     44         View.OnHoverListener, View.OnClickListener {
     45 
     46     private static final float DIALPAD_SLIDE_FRACTION = 1.0f;
     47 
     48     /**
     49      * LinearLayout with getter and setter methods for the translationY property using floats,
     50      * for animation purposes.
     51      */
     52     public static class DialpadSlidingLinearLayout extends LinearLayout {
     53 
     54         public DialpadSlidingLinearLayout(Context context) {
     55             super(context);
     56         }
     57 
     58         public DialpadSlidingLinearLayout(Context context, AttributeSet attrs) {
     59             super(context, attrs);
     60         }
     61 
     62         public DialpadSlidingLinearLayout(Context context, AttributeSet attrs, int defStyle) {
     63             super(context, attrs, defStyle);
     64         }
     65 
     66         public float getYFraction() {
     67             final int height = getHeight();
     68             if (height == 0) return 0;
     69             return getTranslationY() / height;
     70         }
     71 
     72         public void setYFraction(float yFraction) {
     73             setTranslationY(yFraction * getHeight());
     74         }
     75     }
     76 
     77     /**
     78      * LinearLayout that always returns true for onHoverEvent callbacks, to fix
     79      * problems with accessibility due to the dialpad overlaying other fragments.
     80      */
     81     public static class HoverIgnoringLinearLayout extends LinearLayout {
     82 
     83         public HoverIgnoringLinearLayout(Context context) {
     84             super(context);
     85         }
     86 
     87         public HoverIgnoringLinearLayout(Context context, AttributeSet attrs) {
     88             super(context, attrs);
     89         }
     90 
     91         public HoverIgnoringLinearLayout(Context context, AttributeSet attrs, int defStyle) {
     92             super(context, attrs, defStyle);
     93         }
     94 
     95         @Override
     96         public boolean onHoverEvent(MotionEvent event) {
     97             return true;
     98         }
     99     }
    100 
    101     private EditText mDtmfDialerField;
    102 
    103     /** Hash Map to map a view id to a character*/
    104     private static final HashMap<Integer, Character> mDisplayMap =
    105         new HashMap<Integer, Character>();
    106 
    107     /** Set up the static maps*/
    108     static {
    109         // Map the buttons to the display characters
    110         mDisplayMap.put(R.id.one, '1');
    111         mDisplayMap.put(R.id.two, '2');
    112         mDisplayMap.put(R.id.three, '3');
    113         mDisplayMap.put(R.id.four, '4');
    114         mDisplayMap.put(R.id.five, '5');
    115         mDisplayMap.put(R.id.six, '6');
    116         mDisplayMap.put(R.id.seven, '7');
    117         mDisplayMap.put(R.id.eight, '8');
    118         mDisplayMap.put(R.id.nine, '9');
    119         mDisplayMap.put(R.id.zero, '0');
    120         mDisplayMap.put(R.id.pound, '#');
    121         mDisplayMap.put(R.id.star, '*');
    122     }
    123 
    124     // KeyListener used with the "dialpad digits" EditText widget.
    125     private DTMFKeyListener mDialerKeyListener;
    126 
    127     /**
    128      * Our own key listener, specialized for dealing with DTMF codes.
    129      *   1. Ignore the backspace since it is irrelevant.
    130      *   2. Allow ONLY valid DTMF characters to generate a tone and be
    131      *      sent as a DTMF code.
    132      *   3. All other remaining characters are handled by the superclass.
    133      *
    134      * This code is purely here to handle events from the hardware keyboard
    135      * while the DTMF dialpad is up.
    136      */
    137     private class DTMFKeyListener extends DialerKeyListener {
    138 
    139         private DTMFKeyListener() {
    140             super();
    141         }
    142 
    143         /**
    144          * Overriden to return correct DTMF-dialable characters.
    145          */
    146         @Override
    147         protected char[] getAcceptedChars(){
    148             return DTMF_CHARACTERS;
    149         }
    150 
    151         /** special key listener ignores backspace. */
    152         @Override
    153         public boolean backspace(View view, Editable content, int keyCode,
    154                 KeyEvent event) {
    155             return false;
    156         }
    157 
    158         /**
    159          * Return true if the keyCode is an accepted modifier key for the
    160          * dialer (ALT or SHIFT).
    161          */
    162         private boolean isAcceptableModifierKey(int keyCode) {
    163             switch (keyCode) {
    164                 case KeyEvent.KEYCODE_ALT_LEFT:
    165                 case KeyEvent.KEYCODE_ALT_RIGHT:
    166                 case KeyEvent.KEYCODE_SHIFT_LEFT:
    167                 case KeyEvent.KEYCODE_SHIFT_RIGHT:
    168                     return true;
    169                 default:
    170                     return false;
    171             }
    172         }
    173 
    174         /**
    175          * Overriden so that with each valid button press, we start sending
    176          * a dtmf code and play a local dtmf tone.
    177          */
    178         @Override
    179         public boolean onKeyDown(View view, Editable content,
    180                                  int keyCode, KeyEvent event) {
    181             // if (DBG) log("DTMFKeyListener.onKeyDown, keyCode " + keyCode + ", view " + view);
    182 
    183             // find the character
    184             char c = (char) lookup(event, content);
    185 
    186             // if not a long press, and parent onKeyDown accepts the input
    187             if (event.getRepeatCount() == 0 && super.onKeyDown(view, content, keyCode, event)) {
    188 
    189                 boolean keyOK = ok(getAcceptedChars(), c);
    190 
    191                 // if the character is a valid dtmf code, start playing the tone and send the
    192                 // code.
    193                 if (keyOK) {
    194                     Log.d(this, "DTMFKeyListener reading '" + c + "' from input.");
    195                     getPresenter().processDtmf(c);
    196                 } else {
    197                     Log.d(this, "DTMFKeyListener rejecting '" + c + "' from input.");
    198                 }
    199                 return true;
    200             }
    201             return false;
    202         }
    203 
    204         /**
    205          * Overriden so that with each valid button up, we stop sending
    206          * a dtmf code and the dtmf tone.
    207          */
    208         @Override
    209         public boolean onKeyUp(View view, Editable content,
    210                                  int keyCode, KeyEvent event) {
    211             // if (DBG) log("DTMFKeyListener.onKeyUp, keyCode " + keyCode + ", view " + view);
    212 
    213             super.onKeyUp(view, content, keyCode, event);
    214 
    215             // find the character
    216             char c = (char) lookup(event, content);
    217 
    218             boolean keyOK = ok(getAcceptedChars(), c);
    219 
    220             if (keyOK) {
    221                 Log.d(this, "Stopping the tone for '" + c + "'");
    222                 getPresenter().stopTone();
    223                 return true;
    224             }
    225 
    226             return false;
    227         }
    228 
    229         /**
    230          * Handle individual keydown events when we DO NOT have an Editable handy.
    231          */
    232         public boolean onKeyDown(KeyEvent event) {
    233             char c = lookup(event);
    234             Log.d(this, "DTMFKeyListener.onKeyDown: event '" + c + "'");
    235 
    236             // if not a long press, and parent onKeyDown accepts the input
    237             if (event.getRepeatCount() == 0 && c != 0) {
    238                 // if the character is a valid dtmf code, start playing the tone and send the
    239                 // code.
    240                 if (ok(getAcceptedChars(), c)) {
    241                     Log.d(this, "DTMFKeyListener reading '" + c + "' from input.");
    242                     getPresenter().processDtmf(c);
    243                     return true;
    244                 } else {
    245                     Log.d(this, "DTMFKeyListener rejecting '" + c + "' from input.");
    246                 }
    247             }
    248             return false;
    249         }
    250 
    251         /**
    252          * Handle individual keyup events.
    253          *
    254          * @param event is the event we are trying to stop.  If this is null,
    255          * then we just force-stop the last tone without checking if the event
    256          * is an acceptable dialer event.
    257          */
    258         public boolean onKeyUp(KeyEvent event) {
    259             if (event == null) {
    260                 //the below piece of code sends stopDTMF event unnecessarily even when a null event
    261                 //is received, hence commenting it.
    262                 /*if (DBG) log("Stopping the last played tone.");
    263                 stopTone();*/
    264                 return true;
    265             }
    266 
    267             char c = lookup(event);
    268             Log.d(this, "DTMFKeyListener.onKeyUp: event '" + c + "'");
    269 
    270             // TODO: stopTone does not take in character input, we may want to
    271             // consider checking for this ourselves.
    272             if (ok(getAcceptedChars(), c)) {
    273                 Log.d(this, "Stopping the tone for '" + c + "'");
    274                 getPresenter().stopTone();
    275                 return true;
    276             }
    277 
    278             return false;
    279         }
    280 
    281         /**
    282          * Find the Dialer Key mapped to this event.
    283          *
    284          * @return The char value of the input event, otherwise
    285          * 0 if no matching character was found.
    286          */
    287         private char lookup(KeyEvent event) {
    288             // This code is similar to {@link DialerKeyListener#lookup(KeyEvent, Spannable) lookup}
    289             int meta = event.getMetaState();
    290             int number = event.getNumber();
    291 
    292             if (!((meta & (KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON)) == 0) || (number == 0)) {
    293                 int match = event.getMatch(getAcceptedChars(), meta);
    294                 number = (match != 0) ? match : number;
    295             }
    296 
    297             return (char) number;
    298         }
    299 
    300         /**
    301          * Check to see if the keyEvent is dialable.
    302          */
    303         boolean isKeyEventAcceptable (KeyEvent event) {
    304             return (ok(getAcceptedChars(), lookup(event)));
    305         }
    306 
    307         /**
    308          * Overrides the characters used in {@link DialerKeyListener#CHARACTERS}
    309          * These are the valid dtmf characters.
    310          */
    311         public final char[] DTMF_CHARACTERS = new char[] {
    312             '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '*'
    313         };
    314     }
    315 
    316     @Override
    317     public void onClick(View v) {
    318         final AccessibilityManager accessibilityManager = (AccessibilityManager)
    319             v.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
    320         // When accessibility is on, simulate press and release to preserve the
    321         // semantic meaning of performClick(). Required for Braille support.
    322         if (accessibilityManager.isEnabled()) {
    323             final int id = v.getId();
    324             // Checking the press state prevents double activation.
    325             if (!v.isPressed() && mDisplayMap.containsKey(id)) {
    326                 getPresenter().processDtmf(mDisplayMap.get(id), true /* timedShortTone */);
    327             }
    328         }
    329     }
    330 
    331     @Override
    332     public boolean onHover(View v, MotionEvent event) {
    333         // When touch exploration is turned on, lifting a finger while inside
    334         // the button's hover target bounds should perform a click action.
    335         final AccessibilityManager accessibilityManager = (AccessibilityManager)
    336             v.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
    337 
    338         if (accessibilityManager.isEnabled()
    339                 && accessibilityManager.isTouchExplorationEnabled()) {
    340             final int left = v.getPaddingLeft();
    341             final int right = (v.getWidth() - v.getPaddingRight());
    342             final int top = v.getPaddingTop();
    343             final int bottom = (v.getHeight() - v.getPaddingBottom());
    344 
    345             switch (event.getActionMasked()) {
    346                 case MotionEvent.ACTION_HOVER_ENTER:
    347                     // Lift-to-type temporarily disables double-tap activation.
    348                     v.setClickable(false);
    349                     break;
    350                 case MotionEvent.ACTION_HOVER_EXIT:
    351                     final int x = (int) event.getX();
    352                     final int y = (int) event.getY();
    353                     if ((x > left) && (x < right) && (y > top) && (y < bottom)) {
    354                         v.performClick();
    355                     }
    356                     v.setClickable(true);
    357                     break;
    358             }
    359         }
    360 
    361         return false;
    362     }
    363 
    364     @Override
    365     public boolean onKey(View v, int keyCode, KeyEvent event) {
    366         Log.d(this, "onKey:  keyCode " + keyCode + ", view " + v);
    367 
    368         if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
    369             int viewId = v.getId();
    370             if (mDisplayMap.containsKey(viewId)) {
    371                 switch (event.getAction()) {
    372                 case KeyEvent.ACTION_DOWN:
    373                     if (event.getRepeatCount() == 0) {
    374                         getPresenter().processDtmf(mDisplayMap.get(viewId));
    375                     }
    376                     break;
    377                 case KeyEvent.ACTION_UP:
    378                     getPresenter().stopTone();
    379                     break;
    380                 }
    381                 // do not return true [handled] here, since we want the
    382                 // press / click animation to be handled by the framework.
    383             }
    384         }
    385         return false;
    386     }
    387 
    388     @Override
    389     public boolean onTouch(View v, MotionEvent event) {
    390         Log.d(this, "onTouch");
    391         int viewId = v.getId();
    392 
    393         // if the button is recognized
    394         if (mDisplayMap.containsKey(viewId)) {
    395             switch (event.getAction()) {
    396                 case MotionEvent.ACTION_DOWN:
    397                     // Append the character mapped to this button, to the display.
    398                     // start the tone
    399                     getPresenter().processDtmf(mDisplayMap.get(viewId));
    400                     break;
    401                 case MotionEvent.ACTION_UP:
    402                 case MotionEvent.ACTION_CANCEL:
    403                     // stop the tone on ANY other event, except for MOVE.
    404                     getPresenter().stopTone();
    405                     break;
    406             }
    407             // do not return true [handled] here, since we want the
    408             // press / click animation to be handled by the framework.
    409         }
    410         return false;
    411     }
    412 
    413     // TODO(klp) Adds hardware keyboard listener
    414 
    415     @Override
    416     DialpadPresenter createPresenter() {
    417         return new DialpadPresenter();
    418     }
    419 
    420     @Override
    421     DialpadPresenter.DialpadUi getUi() {
    422         return this;
    423     }
    424 
    425     @Override
    426     public void onCreate(Bundle savedInstanceState) {
    427         super.onCreate(savedInstanceState);
    428     }
    429 
    430     @Override
    431     public View onCreateView(LayoutInflater inflater, ViewGroup container,
    432             Bundle savedInstanceState) {
    433         final View parent = inflater.inflate(
    434                 com.android.incallui.R.layout.dtmf_twelve_key_dialer_view, container, false);
    435         mDtmfDialerField = (EditText) parent.findViewById(R.id.dtmfDialerField);
    436         if (mDtmfDialerField != null) {
    437             mDialerKeyListener = new DTMFKeyListener();
    438             mDtmfDialerField.setKeyListener(mDialerKeyListener);
    439             // remove the long-press context menus that support
    440             // the edit (copy / paste / select) functions.
    441             mDtmfDialerField.setLongClickable(false);
    442 
    443             setupKeypad(parent);
    444         }
    445 
    446         final ViewTreeObserver vto = parent.getViewTreeObserver();
    447         // Adjust the translation of the DialpadFragment in a preDrawListener instead of in
    448         // DialtactsActivity, because at the point in time when the DialpadFragment is added,
    449         // its views have not been laid out yet.
    450         final ViewTreeObserver.OnPreDrawListener
    451                 preDrawListener = new ViewTreeObserver.OnPreDrawListener() {
    452             @Override
    453             public boolean onPreDraw() {
    454                 if (isHidden()) return true;
    455                 if (parent.getTranslationY() == 0) {
    456                     ((DialpadSlidingLinearLayout) parent)
    457                             .setYFraction(DIALPAD_SLIDE_FRACTION);
    458                 }
    459                 final ViewTreeObserver vto = parent.getViewTreeObserver();
    460                 vto.removeOnPreDrawListener(this);
    461                 return true;
    462             }
    463 
    464         };
    465 
    466         vto.addOnPreDrawListener(preDrawListener);
    467 
    468         return parent;
    469     }
    470 
    471     @Override
    472     public void onDestroyView() {
    473         mDialerKeyListener = null;
    474         super.onDestroyView();
    475     }
    476 
    477     @Override
    478     public void setVisible(boolean on) {
    479         if (on) {
    480             getView().setVisibility(View.VISIBLE);
    481         } else {
    482             getView().setVisibility(View.INVISIBLE);
    483         }
    484     }
    485 
    486     @Override
    487     public void appendDigitsToField(char digit) {
    488         if (mDtmfDialerField != null) {
    489             // TODO: maybe *don't* manually append this digit if
    490             // mDialpadDigits is focused and this key came from the HW
    491             // keyboard, since in that case the EditText field will
    492             // get the key event directly and automatically appends
    493             // whetever the user types.
    494             // (Or, a cleaner fix would be to just make mDialpadDigits
    495             // *not* handle HW key presses.  That seems to be more
    496             // complicated than just setting focusable="false" on it,
    497             // though.)
    498             mDtmfDialerField.getText().append(digit);
    499         }
    500     }
    501 
    502     /**
    503      * Called externally (from InCallScreen) to play a DTMF Tone.
    504      */
    505     /* package */ boolean onDialerKeyDown(KeyEvent event) {
    506         Log.d(this, "Notifying dtmf key down.");
    507         if (mDialerKeyListener != null) {
    508             return mDialerKeyListener.onKeyDown(event);
    509         } else {
    510             return false;
    511         }
    512     }
    513 
    514     /**
    515      * Called externally (from InCallScreen) to cancel the last DTMF Tone played.
    516      */
    517     public boolean onDialerKeyUp(KeyEvent event) {
    518         Log.d(this, "Notifying dtmf key up.");
    519         if (mDialerKeyListener != null) {
    520             return mDialerKeyListener.onKeyUp(event);
    521         } else {
    522             return false;
    523         }
    524     }
    525 
    526     private void setupKeypad(View fragmentView) {
    527         final int[] buttonIds = new int[] {R.id.zero, R.id.one, R.id.two, R.id.three, R.id.four,
    528                 R.id.five, R.id.six, R.id.seven, R.id.eight, R.id.nine, R.id.star, R.id.pound};
    529 
    530         final int[] numberIds = new int[] {R.string.dialpad_0_number, R.string.dialpad_1_number,
    531                 R.string.dialpad_2_number, R.string.dialpad_3_number, R.string.dialpad_4_number,
    532                 R.string.dialpad_5_number, R.string.dialpad_6_number, R.string.dialpad_7_number,
    533                 R.string.dialpad_8_number, R.string.dialpad_9_number, R.string.dialpad_star_number,
    534                 R.string.dialpad_pound_number};
    535 
    536         final int[] letterIds = new int[] {R.string.dialpad_0_letters, R.string.dialpad_1_letters,
    537                 R.string.dialpad_2_letters, R.string.dialpad_3_letters, R.string.dialpad_4_letters,
    538                 R.string.dialpad_5_letters, R.string.dialpad_6_letters, R.string.dialpad_7_letters,
    539                 R.string.dialpad_8_letters, R.string.dialpad_9_letters,
    540                 R.string.dialpad_star_letters, R.string.dialpad_pound_letters};
    541 
    542         final Resources resources = getResources();
    543 
    544         View button;
    545         TextView numberView;
    546         TextView lettersView;
    547 
    548         for (int i = 0; i < buttonIds.length; i++) {
    549             button = fragmentView.findViewById(buttonIds[i]);
    550             button.setOnTouchListener(this);
    551             button.setClickable(true);
    552             button.setOnKeyListener(this);
    553             button.setOnHoverListener(this);
    554             button.setOnClickListener(this);
    555             numberView = (TextView) button.findViewById(R.id.dialpad_key_number);
    556             lettersView = (TextView) button.findViewById(R.id.dialpad_key_letters);
    557             final String numberString = resources.getString(numberIds[i]);
    558             numberView.setText(numberString);
    559             button.setContentDescription(numberString);
    560             if (lettersView != null) {
    561                 lettersView.setText(resources.getString(letterIds[i]));
    562             }
    563         }
    564     }
    565 }
    566