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 
17 package com.android.phone.common.dialpad;
18 
19 import android.content.Context;
20 import android.graphics.RectF;
21 import android.os.Bundle;
22 import android.util.AttributeSet;
23 import android.view.MotionEvent;
24 import android.view.View;
25 import android.view.ViewConfiguration;
26 import android.view.accessibility.AccessibilityEvent;
27 import android.view.accessibility.AccessibilityManager;
28 import android.view.accessibility.AccessibilityNodeInfo;
29 import 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  */
46 public 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 {
onPressed(View view, boolean pressed)75         public void onPressed(View view, boolean pressed);
76     }
77 
78     private OnPressedListener mOnPressedListener;
79 
setOnPressedListener(OnPressedListener onPressedListener)80     public void setOnPressedListener(OnPressedListener onPressedListener) {
81         mOnPressedListener = onPressedListener;
82     }
83 
DialpadKeyButton(Context context, AttributeSet attrs)84     public DialpadKeyButton(Context context, AttributeSet attrs) {
85         super(context, attrs);
86         initForAccessibility(context);
87     }
88 
DialpadKeyButton(Context context, AttributeSet attrs, int defStyle)89     public DialpadKeyButton(Context context, AttributeSet attrs, int defStyle) {
90         super(context, attrs, defStyle);
91         initForAccessibility(context);
92     }
93 
initForAccessibility(Context context)94     private void initForAccessibility(Context context) {
95         mAccessibilityManager = (AccessibilityManager) context.getSystemService(
96                 Context.ACCESSIBILITY_SERVICE);
97     }
98 
setLongHoverContentDescription(CharSequence contentDescription)99     public void setLongHoverContentDescription(CharSequence contentDescription) {
100         mLongHoverContentDesc = contentDescription;
101 
102         if (mLongHovered) {
103             super.setContentDescription(mLongHoverContentDesc);
104         }
105     }
106 
107     @Override
setContentDescription(CharSequence contentDescription)108     public void setContentDescription(CharSequence contentDescription) {
109         if (mLongHovered) {
110             mBackupContentDesc = contentDescription;
111         } else {
112             super.setContentDescription(contentDescription);
113         }
114     }
115 
116     @Override
setPressed(boolean pressed)117     public void setPressed(boolean pressed) {
118         super.setPressed(pressed);
119         if (mOnPressedListener != null) {
120             mOnPressedListener.onPressed(this, pressed);
121         }
122     }
123 
124     @Override
onSizeChanged(int w, int h, int oldw, int oldh)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
performAccessibilityAction(int action, Bundle arguments)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
onHoverEvent(MotionEvent event)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      */
simulateClickForAccessibility()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 
setLongHovered(boolean enabled)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 
cancelLongHover()227     private void cancelLongHover() {
228         if (mLongHoverRunnable != null) {
229             removeCallbacks(mLongHoverRunnable);
230         }
231         setLongHovered(false);
232     }
233 }
234