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