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