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