• 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 accessibilityManager;
53 
54   /** Bounds used to filter HOVER_EXIT events. */
55   private RectF hoverBounds = new RectF();
56 
57   /** Whether this view is currently in the long-hover state. */
58   private boolean longHovered;
59 
60   /** Alternate content description for long-hover state. */
61   private CharSequence longHoverContentDesc;
62 
63   /** Backup of standard content description. Used for accessibility. */
64   private CharSequence backupContentDesc;
65 
66   /** Backup of clickable property. Used for accessibility. */
67   private boolean wasClickable;
68 
69   /** Backup of long-clickable property. Used for accessibility. */
70   private boolean wasLongClickable;
71 
72   /** Runnable used to trigger long-click mode for accessibility. */
73   private Runnable longHoverRunnable;
74 
75   private OnPressedListener onPressedListener;
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     this.onPressedListener = onPressedListener;
89   }
90 
initForAccessibility(Context context)91   private void initForAccessibility(Context context) {
92     accessibilityManager =
93         (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
94   }
95 
setLongHoverContentDescription(CharSequence contentDescription)96   public void setLongHoverContentDescription(CharSequence contentDescription) {
97     longHoverContentDesc = contentDescription;
98 
99     if (longHovered) {
100       super.setContentDescription(longHoverContentDesc);
101     }
102   }
103 
104   @Override
setContentDescription(CharSequence contentDescription)105   public void setContentDescription(CharSequence contentDescription) {
106     if (longHovered) {
107       backupContentDesc = 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 (onPressedListener != null) {
117       onPressedListener.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     hoverBounds.left = getPaddingLeft();
126     hoverBounds.right = w - getPaddingRight();
127     hoverBounds.top = getPaddingTop();
128     hoverBounds.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 (accessibilityManager.isEnabled() && accessibilityManager.isTouchExplorationEnabled()) {
146       switch (event.getActionMasked()) {
147         case MotionEvent.ACTION_HOVER_ENTER:
148           // Lift-to-type temporarily disables double-tap activation.
149           wasClickable = isClickable();
150           wasLongClickable = isLongClickable();
151           if (wasLongClickable && longHoverContentDesc != null) {
152             if (longHoverRunnable == null) {
153               longHoverRunnable =
154                   new Runnable() {
155                     @Override
156                     public void run() {
157                       setLongHovered(true);
158                       announceForAccessibility(longHoverContentDesc);
159                     }
160                   };
161             }
162             postDelayed(longHoverRunnable, LONG_HOVER_TIMEOUT);
163           }
164 
165           setClickable(false);
166           setLongClickable(false);
167           break;
168         case MotionEvent.ACTION_HOVER_EXIT:
169           if (hoverBounds.contains(event.getX(), event.getY())) {
170             simulateClickForAccessibility();
171           }
172 
173           cancelLongHover();
174           setClickable(wasClickable);
175           setLongClickable(wasLongClickable);
176           break;
177         default: // No-op
178           break;
179       }
180     }
181 
182     return super.onHoverEvent(event);
183   }
184 
185   /**
186    * When accessibility is on, simulate press and release to preserve the semantic meaning of
187    * performClick(). Required for Braille support.
188    */
simulateClickForAccessibility()189   private void simulateClickForAccessibility() {
190     // Checking the press state prevents double activation.
191     if (isPressed()) {
192       return;
193     }
194 
195     setPressed(true);
196 
197     // Stay consistent with performClick() by sending the event after
198     // setting the pressed state but before performing the action.
199     sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
200 
201     setPressed(false);
202   }
203 
setLongHovered(boolean enabled)204   private void setLongHovered(boolean enabled) {
205     if (longHovered != enabled) {
206       longHovered = enabled;
207 
208       // Switch between normal and alternate description, if available.
209       if (enabled) {
210         backupContentDesc = getContentDescription();
211         super.setContentDescription(longHoverContentDesc);
212       } else {
213         super.setContentDescription(backupContentDesc);
214       }
215     }
216   }
217 
cancelLongHover()218   private void cancelLongHover() {
219     if (longHoverRunnable != null) {
220       removeCallbacks(longHoverRunnable);
221     }
222     setLongHovered(false);
223   }
224 
225   public interface OnPressedListener {
226 
onPressed(View view, boolean pressed)227     void onPressed(View view, boolean pressed);
228   }
229 }
230