summaryrefslogtreecommitdiffstats
path: root/src/com/android/phone/common/dialpad/DialpadKeyButton.java
blob: f2458cca90316457d4675ce167cc401aca055f8e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.phone.common.dialpad;

import android.content.Context;
import android.graphics.RectF;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.FrameLayout;

/**
 * Custom class for dialpad buttons.
 * <p>
 * When touch exploration mode is enabled for accessibility, this class
 * implements the lift-to-type interaction model:
 * <ul>
 * <li>Hovering over the button will cause it to gain accessibility focus
 * <li>Removing the hover pointer while inside the bounds of the button will
 * perform a click action
 * <li>If long-click is supported, hovering over the button for a longer period
 * of time will switch to the long-click action
 * <li>Moving the hover pointer outside of the bounds of the button will restore
 * to the normal click action
 * <ul>
 */
public class DialpadKeyButton extends FrameLayout {
    /** Timeout before switching to long-click accessibility mode. */
    private static final int LONG_HOVER_TIMEOUT = ViewConfiguration.getLongPressTimeout() * 2;

    /** Accessibility manager instance used to check touch exploration state. */
    private AccessibilityManager mAccessibilityManager;

    /** Bounds used to filter HOVER_EXIT events. */
    private RectF mHoverBounds = new RectF();

    /** Whether this view is currently in the long-hover state. */
    private boolean mLongHovered;

    /** Alternate content description for long-hover state. */
    private CharSequence mLongHoverContentDesc;

    /** Backup of standard content description. Used for accessibility. */
    private CharSequence mBackupContentDesc;

    /** Backup of clickable property. Used for accessibility. */
    private boolean mWasClickable;

    /** Backup of long-clickable property. Used for accessibility. */
    private boolean mWasLongClickable;

    /** Runnable used to trigger long-click mode for accessibility. */
    private Runnable mLongHoverRunnable;

    public interface OnPressedListener {
        public void onPressed(View view, boolean pressed);
    }

    private OnPressedListener mOnPressedListener;

    public void setOnPressedListener(OnPressedListener onPressedListener) {
        mOnPressedListener = onPressedListener;
    }

    public DialpadKeyButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        initForAccessibility(context);
    }

    public DialpadKeyButton(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initForAccessibility(context);
    }

    private void initForAccessibility(Context context) {
        mAccessibilityManager = (AccessibilityManager) context.getSystemService(
                Context.ACCESSIBILITY_SERVICE);
    }

    public void setLongHoverContentDescription(CharSequence contentDescription) {
        mLongHoverContentDesc = contentDescription;

        if (mLongHovered) {
            super.setContentDescription(mLongHoverContentDesc);
        }
    }

    @Override
    public void setContentDescription(CharSequence contentDescription) {
        if (mLongHovered) {
            mBackupContentDesc = contentDescription;
        } else {
            super.setContentDescription(contentDescription);
        }
    }

    @Override
    public void setPressed(boolean pressed) {
        super.setPressed(pressed);
        if (mOnPressedListener != null) {
            mOnPressedListener.onPressed(this, pressed);
        }
    }

    @Override
    public void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        mHoverBounds.left = getPaddingLeft();
        mHoverBounds.right = w - getPaddingRight();
        mHoverBounds.top = getPaddingTop();
        mHoverBounds.bottom = h - getPaddingBottom();
    }

    @Override
    public boolean performAccessibilityAction(int action, Bundle arguments) {
        if (action == AccessibilityNodeInfo.ACTION_CLICK) {
            simulateClickForAccessibility();
            return true;
        }

        return super.performAccessibilityAction(action, arguments);
    }

    @Override
    public boolean onHoverEvent(MotionEvent event) {
        // When touch exploration is turned on, lifting a finger while inside
        // the button's hover target bounds should perform a click action.
        if (mAccessibilityManager.isEnabled()
                && mAccessibilityManager.isTouchExplorationEnabled()) {
            switch (event.getActionMasked()) {
                case MotionEvent.ACTION_HOVER_ENTER:
                    // Lift-to-type temporarily disables double-tap activation.
                    mWasClickable = isClickable();
                    mWasLongClickable = isLongClickable();
                    if (mWasLongClickable && mLongHoverContentDesc != null) {
                        if (mLongHoverRunnable == null) {
                            mLongHoverRunnable = new Runnable() {
                                @Override
                                public void run() {
                                    setLongHovered(true);
                                    announceForAccessibility(mLongHoverContentDesc);
                                }
                            };
                        }
                        postDelayed(mLongHoverRunnable, LONG_HOVER_TIMEOUT);
                    }

                    setClickable(false);
                    setLongClickable(false);
                    break;
                case MotionEvent.ACTION_HOVER_EXIT:
                    if (mHoverBounds.contains(event.getX(), event.getY())) {
                        if (mLongHovered) {
                            // In accessibility mode the long press will not automatically cause
                            // the short press to fire for the button, so we will fire it now to
                            // emulate the same behavior (this is important for the 0 button).
                            simulateClickForAccessibility();
                            performLongClick();
                        } else {
                            simulateClickForAccessibility();
                        }
                    }

                    cancelLongHover();
                    setClickable(mWasClickable);
                    setLongClickable(mWasLongClickable);
                    break;
            }
        }

        return super.onHoverEvent(event);
    }

    /**
     * When accessibility is on, simulate press and release to preserve the
     * semantic meaning of performClick(). Required for Braille support.
     */
    private void simulateClickForAccessibility() {
        // Checking the press state prevents double activation.
        if (isPressed()) {
            return;
        }

        setPressed(true);

        // Stay consistent with performClick() by sending the event after
        // setting the pressed state but before performing the action.
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        setPressed(false);
    }

    private void setLongHovered(boolean enabled) {
        if (mLongHovered != enabled) {
            mLongHovered = enabled;

            // Switch between normal and alternate description, if available.
            if (enabled) {
                mBackupContentDesc = getContentDescription();
                super.setContentDescription(mLongHoverContentDesc);
            } else {
                super.setContentDescription(mBackupContentDesc);
            }
        }
    }

    private void cancelLongHover() {
        if (mLongHoverRunnable != null) {
            removeCallbacks(mLongHoverRunnable);
        }
        setLongHovered(false);
    }
}