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