1/*
2 * Copyright (C) 2012 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.phone.common.dialpad;
18
19import android.content.Context;
20import android.graphics.RectF;
21import android.os.Bundle;
22import android.util.AttributeSet;
23import android.view.MotionEvent;
24import android.view.View;
25import android.view.ViewConfiguration;
26import android.view.accessibility.AccessibilityEvent;
27import android.view.accessibility.AccessibilityManager;
28import android.view.accessibility.AccessibilityNodeInfo;
29import android.widget.FrameLayout;
30
31/**
32 * Custom class for dialpad buttons.
33 * <p>
34 * When touch exploration mode is enabled for accessibility, this class
35 * implements the lift-to-type interaction model:
36 * <ul>
37 * <li>Hovering over the button will cause it to gain accessibility focus
38 * <li>Removing the hover pointer while inside the bounds of the button will
39 * perform a click action
40 * <li>If long-click is supported, hovering over the button for a longer period
41 * of time will switch to the long-click action
42 * <li>Moving the hover pointer outside of the bounds of the button will restore
43 * to the normal click action
44 * <ul>
45 */
46public class DialpadKeyButton extends FrameLayout {
47    /** Timeout before switching to long-click accessibility mode. */
48    private static final int LONG_HOVER_TIMEOUT = ViewConfiguration.getLongPressTimeout() * 2;
49
50    /** Accessibility manager instance used to check touch exploration state. */
51    private AccessibilityManager mAccessibilityManager;
52
53    /** Bounds used to filter HOVER_EXIT events. */
54    private RectF mHoverBounds = new RectF();
55
56    /** Whether this view is currently in the long-hover state. */
57    private boolean mLongHovered;
58
59    /** Alternate content description for long-hover state. */
60    private CharSequence mLongHoverContentDesc;
61
62    /** Backup of standard content description. Used for accessibility. */
63    private CharSequence mBackupContentDesc;
64
65    /** Backup of clickable property. Used for accessibility. */
66    private boolean mWasClickable;
67
68    /** Backup of long-clickable property. Used for accessibility. */
69    private boolean mWasLongClickable;
70
71    /** Runnable used to trigger long-click mode for accessibility. */
72    private Runnable mLongHoverRunnable;
73
74    public interface OnPressedListener {
75        public void onPressed(View view, boolean pressed);
76    }
77
78    private OnPressedListener mOnPressedListener;
79
80    public void setOnPressedListener(OnPressedListener onPressedListener) {
81        mOnPressedListener = onPressedListener;
82    }
83
84    public DialpadKeyButton(Context context, AttributeSet attrs) {
85        super(context, attrs);
86        initForAccessibility(context);
87    }
88
89    public DialpadKeyButton(Context context, AttributeSet attrs, int defStyle) {
90        super(context, attrs, defStyle);
91        initForAccessibility(context);
92    }
93
94    private void initForAccessibility(Context context) {
95        mAccessibilityManager = (AccessibilityManager) context.getSystemService(
96                Context.ACCESSIBILITY_SERVICE);
97    }
98
99    public void setLongHoverContentDescription(CharSequence contentDescription) {
100        mLongHoverContentDesc = contentDescription;
101
102        if (mLongHovered) {
103            super.setContentDescription(mLongHoverContentDesc);
104        }
105    }
106
107    @Override
108    public void setContentDescription(CharSequence contentDescription) {
109        if (mLongHovered) {
110            mBackupContentDesc = contentDescription;
111        } else {
112            super.setContentDescription(contentDescription);
113        }
114    }
115
116    @Override
117    public void setPressed(boolean pressed) {
118        super.setPressed(pressed);
119        if (mOnPressedListener != null) {
120            mOnPressedListener.onPressed(this, pressed);
121        }
122    }
123
124    @Override
125    public void onSizeChanged(int w, int h, int oldw, int oldh) {
126        super.onSizeChanged(w, h, oldw, oldh);
127
128        mHoverBounds.left = getPaddingLeft();
129        mHoverBounds.right = w - getPaddingRight();
130        mHoverBounds.top = getPaddingTop();
131        mHoverBounds.bottom = h - getPaddingBottom();
132    }
133
134    @Override
135    public boolean performAccessibilityAction(int action, Bundle arguments) {
136        if (action == AccessibilityNodeInfo.ACTION_CLICK) {
137            simulateClickForAccessibility();
138            return true;
139        }
140
141        return super.performAccessibilityAction(action, arguments);
142    }
143
144    @Override
145    public boolean onHoverEvent(MotionEvent event) {
146        // When touch exploration is turned on, lifting a finger while inside
147        // the button's hover target bounds should perform a click action.
148        if (mAccessibilityManager.isEnabled()
149                && mAccessibilityManager.isTouchExplorationEnabled()) {
150            switch (event.getActionMasked()) {
151                case MotionEvent.ACTION_HOVER_ENTER:
152                    // Lift-to-type temporarily disables double-tap activation.
153                    mWasClickable = isClickable();
154                    mWasLongClickable = isLongClickable();
155                    if (mWasLongClickable && mLongHoverContentDesc != null) {
156                        if (mLongHoverRunnable == null) {
157                            mLongHoverRunnable = new Runnable() {
158                                @Override
159                                public void run() {
160                                    setLongHovered(true);
161                                    announceForAccessibility(mLongHoverContentDesc);
162                                }
163                            };
164                        }
165                        postDelayed(mLongHoverRunnable, LONG_HOVER_TIMEOUT);
166                    }
167
168                    setClickable(false);
169                    setLongClickable(false);
170                    break;
171                case MotionEvent.ACTION_HOVER_EXIT:
172                    if (mHoverBounds.contains(event.getX(), event.getY())) {
173                        if (mLongHovered) {
174                            // In accessibility mode the long press will not automatically cause
175                            // the short press to fire for the button, so we will fire it now to
176                            // emulate the same behavior (this is important for the 0 button).
177                            simulateClickForAccessibility();
178                            performLongClick();
179                        } else {
180                            simulateClickForAccessibility();
181                        }
182                    }
183
184                    cancelLongHover();
185                    setClickable(mWasClickable);
186                    setLongClickable(mWasLongClickable);
187                    break;
188            }
189        }
190
191        return super.onHoverEvent(event);
192    }
193
194    /**
195     * When accessibility is on, simulate press and release to preserve the
196     * semantic meaning of performClick(). Required for Braille support.
197     */
198    private void simulateClickForAccessibility() {
199        // Checking the press state prevents double activation.
200        if (isPressed()) {
201            return;
202        }
203
204        setPressed(true);
205
206        // Stay consistent with performClick() by sending the event after
207        // setting the pressed state but before performing the action.
208        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
209
210        setPressed(false);
211    }
212
213    private void setLongHovered(boolean enabled) {
214        if (mLongHovered != enabled) {
215            mLongHovered = enabled;
216
217            // Switch between normal and alternate description, if available.
218            if (enabled) {
219                mBackupContentDesc = getContentDescription();
220                super.setContentDescription(mLongHoverContentDesc);
221            } else {
222                super.setContentDescription(mBackupContentDesc);
223            }
224        }
225    }
226
227    private void cancelLongHover() {
228        if (mLongHoverRunnable != null) {
229            removeCallbacks(mLongHoverRunnable);
230        }
231        setLongHovered(false);
232    }
233}
234