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