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 { 75 public void onPressed(View view, boolean pressed); 76 } 77 78 private OnPressedListener mOnPressedListener; 79 80 public void setOnPressedListener(OnPressedListener onPressedListener) { 81 mOnPressedListener = onPressedListener; 82 } 83 84 public DialpadKeyButton(Context context, AttributeSet attrs) { 85 super(context, attrs); 86 initForAccessibility(context); 87 } 88 89 public DialpadKeyButton(Context context, AttributeSet attrs, int defStyle) { 90 super(context, attrs, defStyle); 91 initForAccessibility(context); 92 } 93 94 private void initForAccessibility(Context context) { 95 mAccessibilityManager = (AccessibilityManager) context.getSystemService( 96 Context.ACCESSIBILITY_SERVICE); 97 } 98 99 public void setLongHoverContentDescription(CharSequence contentDescription) { 100 mLongHoverContentDesc = contentDescription; 101 102 if (mLongHovered) { 103 super.setContentDescription(mLongHoverContentDesc); 104 } 105 } 106 107 @Override 108 public void setContentDescription(CharSequence contentDescription) { 109 if (mLongHovered) { 110 mBackupContentDesc = contentDescription; 111 } else { 112 super.setContentDescription(contentDescription); 113 } 114 } 115 116 @Override 117 public void setPressed(boolean pressed) { 118 super.setPressed(pressed); 119 if (mOnPressedListener != null) { 120 mOnPressedListener.onPressed(this, pressed); 121 } 122 } 123 124 @Override 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 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 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 */ 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 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 227 private void cancelLongHover() { 228 if (mLongHoverRunnable != null) { 229 removeCallbacks(mLongHoverRunnable); 230 } 231 setLongHovered(false); 232 } 233 } 234