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.text.TextUtils; 23 import android.util.AttributeSet; 24 import android.view.MotionEvent; 25 import android.view.View; 26 import android.view.accessibility.AccessibilityEvent; 27 import android.view.accessibility.AccessibilityManager; 28 import android.view.accessibility.AccessibilityNodeInfo; 29 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 30 import android.widget.FrameLayout; 31 32 /** 33 * Custom class for dialpad buttons. 34 * 35 * <p>When touch exploration mode is enabled for accessibility, this class implements the 36 * lift-to-type interaction model: 37 * 38 * <ul> 39 * <li>Hovering over the button will cause it to gain accessibility focus 40 * <li>Removing the hover pointer while inside the bounds of the button will perform a click action 41 * <li>If long-click is supported, hovering over the button for a longer period of time will switch 42 * to the long-click action 43 * <li>Moving the hover pointer outside of the bounds of the button will restore to the normal click 44 * action 45 * <ul> 46 */ 47 public class DialpadKeyButton extends FrameLayout { 48 49 /** Accessibility manager instance used to check touch exploration state. */ 50 private AccessibilityManager accessibilityManager; 51 52 /** Bounds used to filter HOVER_EXIT events. */ 53 private RectF hoverBounds = new RectF(); 54 55 /** Alternate content description for long-hover state. */ 56 private CharSequence longHoverContentDesc; 57 58 /** Backup of clickable property. Used for accessibility. */ 59 private boolean wasClickable; 60 61 /** Backup of long-clickable property. Used for accessibility. */ 62 private boolean wasLongClickable; 63 64 private OnPressedListener onPressedListener; 65 DialpadKeyButton(Context context, AttributeSet attrs)66 public DialpadKeyButton(Context context, AttributeSet attrs) { 67 super(context, attrs); 68 initForAccessibility(context); 69 } 70 DialpadKeyButton(Context context, AttributeSet attrs, int defStyle)71 public DialpadKeyButton(Context context, AttributeSet attrs, int defStyle) { 72 super(context, attrs, defStyle); 73 initForAccessibility(context); 74 } 75 setOnPressedListener(OnPressedListener onPressedListener)76 public void setOnPressedListener(OnPressedListener onPressedListener) { 77 this.onPressedListener = onPressedListener; 78 } 79 initForAccessibility(Context context)80 private void initForAccessibility(Context context) { 81 accessibilityManager = 82 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 83 } 84 setLongHoverContentDescription(CharSequence contentDescription)85 public void setLongHoverContentDescription(CharSequence contentDescription) { 86 longHoverContentDesc = contentDescription; 87 } 88 89 @Override setPressed(boolean pressed)90 public void setPressed(boolean pressed) { 91 super.setPressed(pressed); 92 if (onPressedListener != null) { 93 onPressedListener.onPressed(this, pressed); 94 } 95 } 96 97 @Override onSizeChanged(int w, int h, int oldw, int oldh)98 public void onSizeChanged(int w, int h, int oldw, int oldh) { 99 super.onSizeChanged(w, h, oldw, oldh); 100 101 hoverBounds.left = getPaddingLeft(); 102 hoverBounds.right = w - getPaddingRight(); 103 hoverBounds.top = getPaddingTop(); 104 hoverBounds.bottom = h - getPaddingBottom(); 105 } 106 107 @Override performAccessibilityAction(int action, Bundle arguments)108 public boolean performAccessibilityAction(int action, Bundle arguments) { 109 if (action == AccessibilityNodeInfo.ACTION_CLICK) { 110 simulateClickForAccessibility(); 111 return true; 112 } 113 114 return super.performAccessibilityAction(action, arguments); 115 } 116 117 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)118 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 119 super.onInitializeAccessibilityNodeInfo(info); 120 // If the button has a long hover description, ask talkback to announce the action follow by 121 // the description (for example "double tap and hold to call voicemail"). 122 if (!TextUtils.isEmpty(longHoverContentDesc)) { 123 AccessibilityAction longClickAction = 124 new AccessibilityAction(AccessibilityNodeInfo.ACTION_LONG_CLICK, longHoverContentDesc); 125 info.addAction(longClickAction); 126 } 127 } 128 129 @Override onHoverEvent(MotionEvent event)130 public boolean onHoverEvent(MotionEvent event) { 131 // When touch exploration is turned on, lifting a finger while inside 132 // the button's hover target bounds should perform a click action. 133 if (accessibilityManager.isEnabled() && accessibilityManager.isTouchExplorationEnabled()) { 134 switch (event.getActionMasked()) { 135 case MotionEvent.ACTION_HOVER_ENTER: 136 // Lift-to-type temporarily disables double-tap activation. 137 wasClickable = isClickable(); 138 wasLongClickable = isLongClickable(); 139 setClickable(false); 140 setLongClickable(false); 141 break; 142 case MotionEvent.ACTION_HOVER_EXIT: 143 if (hoverBounds.contains(event.getX(), event.getY())) { 144 simulateClickForAccessibility(); 145 } 146 147 setClickable(wasClickable); 148 setLongClickable(wasLongClickable); 149 break; 150 default: // No-op 151 break; 152 } 153 } 154 155 return super.onHoverEvent(event); 156 } 157 158 /** 159 * When accessibility is on, simulate press and release to preserve the semantic meaning of 160 * performClick(). Required for Braille support. 161 */ simulateClickForAccessibility()162 private void simulateClickForAccessibility() { 163 // Checking the press state prevents double activation. 164 if (isPressed()) { 165 return; 166 } 167 168 setPressed(true); 169 170 // Stay consistent with performClick() by sending the event after 171 // setting the pressed state but before performing the action. 172 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); 173 174 setPressed(false); 175 } 176 177 public interface OnPressedListener { 178 onPressed(View view, boolean pressed)179 void onPressed(View view, boolean pressed); 180 } 181 } 182