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