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