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 { onPressed(View view, boolean pressed)75 public void onPressed(View view, boolean pressed); 76 } 77 78 private OnPressedListener mOnPressedListener; 79 setOnPressedListener(OnPressedListener onPressedListener)80 public void setOnPressedListener(OnPressedListener onPressedListener) { 81 mOnPressedListener = onPressedListener; 82 } 83 DialpadKeyButton(Context context, AttributeSet attrs)84 public DialpadKeyButton(Context context, AttributeSet attrs) { 85 super(context, attrs); 86 initForAccessibility(context); 87 } 88 DialpadKeyButton(Context context, AttributeSet attrs, int defStyle)89 public DialpadKeyButton(Context context, AttributeSet attrs, int defStyle) { 90 super(context, attrs, defStyle); 91 initForAccessibility(context); 92 } 93 initForAccessibility(Context context)94 private void initForAccessibility(Context context) { 95 mAccessibilityManager = (AccessibilityManager) context.getSystemService( 96 Context.ACCESSIBILITY_SERVICE); 97 } 98 setLongHoverContentDescription(CharSequence contentDescription)99 public void setLongHoverContentDescription(CharSequence contentDescription) { 100 mLongHoverContentDesc = contentDescription; 101 102 if (mLongHovered) { 103 super.setContentDescription(mLongHoverContentDesc); 104 } 105 } 106 107 @Override setContentDescription(CharSequence contentDescription)108 public void setContentDescription(CharSequence contentDescription) { 109 if (mLongHovered) { 110 mBackupContentDesc = contentDescription; 111 } else { 112 super.setContentDescription(contentDescription); 113 } 114 } 115 116 @Override setPressed(boolean pressed)117 public void setPressed(boolean pressed) { 118 super.setPressed(pressed); 119 if (mOnPressedListener != null) { 120 mOnPressedListener.onPressed(this, pressed); 121 } 122 } 123 124 @Override onSizeChanged(int w, int h, int oldw, int oldh)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 performAccessibilityAction(int action, Bundle arguments)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 onHoverEvent(MotionEvent event)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 performLongClick(); 175 } else { 176 simulateClickForAccessibility(); 177 } 178 } 179 180 cancelLongHover(); 181 setClickable(mWasClickable); 182 setLongClickable(mWasLongClickable); 183 break; 184 } 185 } 186 187 return super.onHoverEvent(event); 188 } 189 190 /** 191 * When accessibility is on, simulate press and release to preserve the 192 * semantic meaning of performClick(). Required for Braille support. 193 */ simulateClickForAccessibility()194 private void simulateClickForAccessibility() { 195 // Checking the press state prevents double activation. 196 if (isPressed()) { 197 return; 198 } 199 200 setPressed(true); 201 202 // Stay consistent with performClick() by sending the event after 203 // setting the pressed state but before performing the action. 204 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); 205 206 setPressed(false); 207 } 208 setLongHovered(boolean enabled)209 private void setLongHovered(boolean enabled) { 210 if (mLongHovered != enabled) { 211 mLongHovered = enabled; 212 213 // Switch between normal and alternate description, if available. 214 if (enabled) { 215 mBackupContentDesc = getContentDescription(); 216 super.setContentDescription(mLongHoverContentDesc); 217 } else { 218 super.setContentDescription(mBackupContentDesc); 219 } 220 } 221 } 222 cancelLongHover()223 private void cancelLongHover() { 224 if (mLongHoverRunnable != null) { 225 removeCallbacks(mLongHoverRunnable); 226 } 227 setLongHovered(false); 228 } 229 } 230