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 77 public DialpadKeyButton(Context context, AttributeSet attrs) { 78 super(context, attrs); 79 initForAccessibility(context); 80 } 81 82 public DialpadKeyButton(Context context, AttributeSet attrs, int defStyle) { 83 super(context, attrs, defStyle); 84 initForAccessibility(context); 85 } 86 87 public void setOnPressedListener(OnPressedListener onPressedListener) { 88 this.onPressedListener = onPressedListener; 89 } 90 91 private void initForAccessibility(Context context) { 92 accessibilityManager = 93 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 94 } 95 96 public void setLongHoverContentDescription(CharSequence contentDescription) { 97 longHoverContentDesc = contentDescription; 98 99 if (longHovered) { 100 super.setContentDescription(longHoverContentDesc); 101 } 102 } 103 104 @Override 105 public void setContentDescription(CharSequence contentDescription) { 106 if (longHovered) { 107 backupContentDesc = contentDescription; 108 } else { 109 super.setContentDescription(contentDescription); 110 } 111 } 112 113 @Override 114 public void setPressed(boolean pressed) { 115 super.setPressed(pressed); 116 if (onPressedListener != null) { 117 onPressedListener.onPressed(this, pressed); 118 } 119 } 120 121 @Override 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 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 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 */ 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 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 218 private void cancelLongHover() { 219 if (longHoverRunnable != null) { 220 removeCallbacks(longHoverRunnable); 221 } 222 setLongHovered(false); 223 } 224 225 public interface OnPressedListener { 226 227 void onPressed(View view, boolean pressed); 228 } 229 } 230