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