• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
DialpadSlidingLinearLayout(Context context)57         public DialpadSlidingLinearLayout(Context context) {
58             super(context);
59         }
60 
DialpadSlidingLinearLayout(Context context, AttributeSet attrs)61         public DialpadSlidingLinearLayout(Context context, AttributeSet attrs) {
62             super(context, attrs);
63         }
64 
DialpadSlidingLinearLayout(Context context, AttributeSet attrs, int defStyle)65         public DialpadSlidingLinearLayout(Context context, AttributeSet attrs, int defStyle) {
66             super(context, attrs, defStyle);
67         }
68 
getYFraction()69         public float getYFraction() {
70             final int height = getHeight();
71             if (height == 0) return 0;
72             return getTranslationY() / height;
73         }
74 
setYFraction(float yFraction)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 
HoverIgnoringLinearLayout(Context context)86         public HoverIgnoringLinearLayout(Context context) {
87             super(context);
88         }
89 
HoverIgnoringLinearLayout(Context context, AttributeSet attrs)90         public HoverIgnoringLinearLayout(Context context, AttributeSet attrs) {
91             super(context, attrs);
92         }
93 
HoverIgnoringLinearLayout(Context context, AttributeSet attrs, int defStyle)94         public HoverIgnoringLinearLayout(Context context, AttributeSet attrs, int defStyle) {
95             super(context, attrs, defStyle);
96         }
97 
98         @Override
onHoverEvent(MotionEvent event)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
mDisplayMap.put(R.id.one, '1')116         mDisplayMap.put(R.id.one, '1');
mDisplayMap.put(R.id.two, '2')117         mDisplayMap.put(R.id.two, '2');
mDisplayMap.put(R.id.three, '3')118         mDisplayMap.put(R.id.three, '3');
mDisplayMap.put(R.id.four, '4')119         mDisplayMap.put(R.id.four, '4');
mDisplayMap.put(R.id.five, '5')120         mDisplayMap.put(R.id.five, '5');
mDisplayMap.put(R.id.six, '6')121         mDisplayMap.put(R.id.six, '6');
mDisplayMap.put(R.id.seven, '7')122         mDisplayMap.put(R.id.seven, '7');
mDisplayMap.put(R.id.eight, '8')123         mDisplayMap.put(R.id.eight, '8');
mDisplayMap.put(R.id.nine, '9')124         mDisplayMap.put(R.id.nine, '9');
mDisplayMap.put(R.id.zero, '0')125         mDisplayMap.put(R.id.zero, '0');
mDisplayMap.put(R.id.pound, '#')126         mDisplayMap.put(R.id.pound, '#');
mDisplayMap.put(R.id.star, '*')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 
DTMFKeyListener()147         private DTMFKeyListener() {
148             super();
149         }
150 
151         /**
152          * Overriden to return correct DTMF-dialable characters.
153          */
154         @Override
getAcceptedChars()155         protected char[] getAcceptedChars(){
156             return DTMF_CHARACTERS;
157         }
158 
159         /** special key listener ignores backspace. */
160         @Override
backspace(View view, Editable content, int keyCode, KeyEvent event)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          */
isAcceptableModifierKey(int keyCode)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
onKeyDown(View view, Editable content, int keyCode, KeyEvent event)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
onKeyUp(View view, Editable content, int keyCode, KeyEvent event)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          */
onKeyDown(KeyEvent event)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          */
onKeyUp(KeyEvent event)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          */
lookup(KeyEvent event)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          */
isKeyEventAcceptable(KeyEvent event)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
onClick(View v)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
onHover(View v, MotionEvent event)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
onKey(View v, int keyCode, KeyEvent event)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
onTouch(View v, MotionEvent event)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
createPresenter()430     DialpadPresenter createPresenter() {
431         return new DialpadPresenter();
432     }
433 
434     @Override
getUi()435     DialpadPresenter.DialpadUi getUi() {
436         return this;
437     }
438 
439     @Override
onCreate(Bundle savedInstanceState)440     public void onCreate(Bundle savedInstanceState) {
441         super.onCreate(savedInstanceState);
442     }
443 
444     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onDestroyView()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      */
getDtmfText()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      */
setDtmfText(String text)486     public void setDtmfText(String text) {
487         mDtmfDialerField.setText(text);
488     }
489 
490     @Override
setVisible(boolean on)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      */
animateShowDialpad()502     public void animateShowDialpad() {
503         final DialpadView dialpadView = (DialpadView) getView().findViewById(R.id.dialpad_view);
504         dialpadView.animateShow();
505     }
506 
507     @Override
appendDigitsToField(char digit)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      */
onDialerKeyDown(KeyEvent event)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      */
onDialerKeyUp(KeyEvent event)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 
configureKeypadListeners(View fragmentView)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