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