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
17package com.android.incallui;
18
19import android.content.Context;
20import android.os.Bundle;
21import android.os.Handler;
22import android.os.Looper;
23import android.telephony.PhoneNumberUtils;
24import android.text.Editable;
25import android.text.method.DialerKeyListener;
26import android.util.AttributeSet;
27import android.view.KeyEvent;
28import android.view.LayoutInflater;
29import android.view.MotionEvent;
30import android.view.View;
31import android.view.ViewGroup;
32import android.view.accessibility.AccessibilityManager;
33import android.widget.EditText;
34import android.widget.LinearLayout;
35import android.widget.TextView;
36
37import com.android.phone.common.dialpad.DialpadKeyButton;
38import com.android.phone.common.dialpad.DialpadView;
39
40import java.util.HashMap;
41
42/**
43 * Fragment for call control buttons
44 */
45public 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
61        public DialpadSlidingLinearLayout(Context context) {
62            super(context);
63        }
64
65        public DialpadSlidingLinearLayout(Context context, AttributeSet attrs) {
66            super(context, attrs);
67        }
68
69        public DialpadSlidingLinearLayout(Context context, AttributeSet attrs, int defStyle) {
70            super(context, attrs, defStyle);
71        }
72
73        public float getYFraction() {
74            final int height = getHeight();
75            if (height == 0) return 0;
76            return getTranslationY() / height;
77        }
78
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
96        mDisplayMap.put(R.id.one, '1');
97        mDisplayMap.put(R.id.two, '2');
98        mDisplayMap.put(R.id.three, '3');
99        mDisplayMap.put(R.id.four, '4');
100        mDisplayMap.put(R.id.five, '5');
101        mDisplayMap.put(R.id.six, '6');
102        mDisplayMap.put(R.id.seven, '7');
103        mDisplayMap.put(R.id.eight, '8');
104        mDisplayMap.put(R.id.nine, '9');
105        mDisplayMap.put(R.id.zero, '0');
106        mDisplayMap.put(R.id.pound, '#');
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
129        private DTMFKeyListener() {
130            super();
131        }
132
133        /**
134         * Overriden to return correct DTMF-dialable characters.
135         */
136        @Override
137        protected char[] getAcceptedChars(){
138            return DTMF_CHARACTERS;
139        }
140
141        /** special key listener ignores backspace. */
142        @Override
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         */
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
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
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         */
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         */
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         */
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         */
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
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
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
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
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
412    public DialpadPresenter createPresenter() {
413        return new DialpadPresenter();
414    }
415
416    @Override
417    public DialpadPresenter.DialpadUi getUi() {
418        return this;
419    }
420
421    @Override
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
444    public void onResume() {
445        super.onResume();
446        updateColors();
447    }
448
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
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     */
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     */
485    public void setDtmfText(String text) {
486        mDtmfDialerField.setText(PhoneNumberUtils.createTtsSpannable(text));
487    }
488
489    @Override
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     */
501    public void animateShowDialpad() {
502        final DialpadView dialpadView = (DialpadView) getView().findViewById(R.id.dialpad_view);
503        dialpadView.animateShow();
504    }
505
506    @Override
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     */
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     */
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
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