Home | History | Annotate | Download | only in dialpad
      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.Rect;
     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 Rect mHoverBounds = new Rect();
     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((int) event.getX(), (int) 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      */
    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 
    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 
    223     private void cancelLongHover() {
    224         if (mLongHoverRunnable != null) {
    225             removeCallbacks(mLongHoverRunnable);
    226         }
    227         setLongHovered(false);
    228     }
    229 }
    230