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.text.Editable;
24import android.text.method.DialerKeyListener;
25import android.util.AttributeSet;
26import android.view.KeyEvent;
27import android.view.LayoutInflater;
28import android.view.MotionEvent;
29import android.view.View;
30import android.view.ViewGroup;
31import android.view.accessibility.AccessibilityManager;
32import android.widget.EditText;
33import android.widget.LinearLayout;
34import android.widget.TextView;
35
36import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
37import com.android.dialer.R;
38import com.android.phone.common.dialpad.DialpadKeyButton;
39import com.android.phone.common.dialpad.DialpadView;
40
41import java.util.HashMap;
42
43/**
44 * Fragment for call control buttons
45 */
46public 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
62        public DialpadSlidingLinearLayout(Context context) {
63            super(context);
64        }
65
66        public DialpadSlidingLinearLayout(Context context, AttributeSet attrs) {
67            super(context, attrs);
68        }
69
70        public DialpadSlidingLinearLayout(Context context, AttributeSet attrs, int defStyle) {
71            super(context, attrs, defStyle);
72        }
73
74        public float getYFraction() {
75            final int height = getHeight();
76            if (height == 0) return 0;
77            return getTranslationY() / height;
78        }
79
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
97        mDisplayMap.put(R.id.one, '1');
98        mDisplayMap.put(R.id.two, '2');
99        mDisplayMap.put(R.id.three, '3');
100        mDisplayMap.put(R.id.four, '4');
101        mDisplayMap.put(R.id.five, '5');
102        mDisplayMap.put(R.id.six, '6');
103        mDisplayMap.put(R.id.seven, '7');
104        mDisplayMap.put(R.id.eight, '8');
105        mDisplayMap.put(R.id.nine, '9');
106        mDisplayMap.put(R.id.zero, '0');
107        mDisplayMap.put(R.id.pound, '#');
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
130        private DTMFKeyListener() {
131            super();
132        }
133
134        /**
135         * Overriden to return correct DTMF-dialable characters.
136         */
137        @Override
138        protected char[] getAcceptedChars(){
139            return DTMF_CHARACTERS;
140        }
141
142        /** special key listener ignores backspace. */
143        @Override
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         */
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
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
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         */
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         */
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         */
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         */
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
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
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
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
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
416    public DialpadPresenter createPresenter() {
417        return new DialpadPresenter();
418    }
419
420    @Override
421    public DialpadPresenter.DialpadUi getUi() {
422        return this;
423    }
424
425    @Override
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
451    public void onResume() {
452        super.onResume();
453        updateColors();
454    }
455
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
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     */
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     */
492    public void setDtmfText(String text) {
493        mDtmfDialerField.setText(PhoneNumberUtilsCompat.createTtsSpannable(text));
494    }
495
496    @Override
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     */
508    public void animateShowDialpad() {
509        final DialpadView dialpadView = (DialpadView) getView().findViewById(R.id.dialpad_view);
510        dialpadView.animateShow();
511    }
512
513    @Override
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     */
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     */
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
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