Home | History | Annotate | Download | only in dialpadview
      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 accessibilityManager;
     53 
     54   /** Bounds used to filter HOVER_EXIT events. */
     55   private RectF hoverBounds = new RectF();
     56 
     57   /** Whether this view is currently in the long-hover state. */
     58   private boolean longHovered;
     59 
     60   /** Alternate content description for long-hover state. */
     61   private CharSequence longHoverContentDesc;
     62 
     63   /** Backup of standard content description. Used for accessibility. */
     64   private CharSequence backupContentDesc;
     65 
     66   /** Backup of clickable property. Used for accessibility. */
     67   private boolean wasClickable;
     68 
     69   /** Backup of long-clickable property. Used for accessibility. */
     70   private boolean wasLongClickable;
     71 
     72   /** Runnable used to trigger long-click mode for accessibility. */
     73   private Runnable longHoverRunnable;
     74 
     75   private OnPressedListener onPressedListener;
     76 
     77   public DialpadKeyButton(Context context, AttributeSet attrs) {
     78     super(context, attrs);
     79     initForAccessibility(context);
     80   }
     81 
     82   public DialpadKeyButton(Context context, AttributeSet attrs, int defStyle) {
     83     super(context, attrs, defStyle);
     84     initForAccessibility(context);
     85   }
     86 
     87   public void setOnPressedListener(OnPressedListener onPressedListener) {
     88     this.onPressedListener = onPressedListener;
     89   }
     90 
     91   private void initForAccessibility(Context context) {
     92     accessibilityManager =
     93         (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
     94   }
     95 
     96   public void setLongHoverContentDescription(CharSequence contentDescription) {
     97     longHoverContentDesc = contentDescription;
     98 
     99     if (longHovered) {
    100       super.setContentDescription(longHoverContentDesc);
    101     }
    102   }
    103 
    104   @Override
    105   public void setContentDescription(CharSequence contentDescription) {
    106     if (longHovered) {
    107       backupContentDesc = contentDescription;
    108     } else {
    109       super.setContentDescription(contentDescription);
    110     }
    111   }
    112 
    113   @Override
    114   public void setPressed(boolean pressed) {
    115     super.setPressed(pressed);
    116     if (onPressedListener != null) {
    117       onPressedListener.onPressed(this, pressed);
    118     }
    119   }
    120 
    121   @Override
    122   public void onSizeChanged(int w, int h, int oldw, int oldh) {
    123     super.onSizeChanged(w, h, oldw, oldh);
    124 
    125     hoverBounds.left = getPaddingLeft();
    126     hoverBounds.right = w - getPaddingRight();
    127     hoverBounds.top = getPaddingTop();
    128     hoverBounds.bottom = h - getPaddingBottom();
    129   }
    130 
    131   @Override
    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
    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 (accessibilityManager.isEnabled() && accessibilityManager.isTouchExplorationEnabled()) {
    146       switch (event.getActionMasked()) {
    147         case MotionEvent.ACTION_HOVER_ENTER:
    148           // Lift-to-type temporarily disables double-tap activation.
    149           wasClickable = isClickable();
    150           wasLongClickable = isLongClickable();
    151           if (wasLongClickable && longHoverContentDesc != null) {
    152             if (longHoverRunnable == null) {
    153               longHoverRunnable =
    154                   new Runnable() {
    155                     @Override
    156                     public void run() {
    157                       setLongHovered(true);
    158                       announceForAccessibility(longHoverContentDesc);
    159                     }
    160                   };
    161             }
    162             postDelayed(longHoverRunnable, LONG_HOVER_TIMEOUT);
    163           }
    164 
    165           setClickable(false);
    166           setLongClickable(false);
    167           break;
    168         case MotionEvent.ACTION_HOVER_EXIT:
    169           if (hoverBounds.contains(event.getX(), event.getY())) {
    170             simulateClickForAccessibility();
    171           }
    172 
    173           cancelLongHover();
    174           setClickable(wasClickable);
    175           setLongClickable(wasLongClickable);
    176           break;
    177         default: // No-op
    178           break;
    179       }
    180     }
    181 
    182     return super.onHoverEvent(event);
    183   }
    184 
    185   /**
    186    * When accessibility is on, simulate press and release to preserve the semantic meaning of
    187    * performClick(). Required for Braille support.
    188    */
    189   private void simulateClickForAccessibility() {
    190     // Checking the press state prevents double activation.
    191     if (isPressed()) {
    192       return;
    193     }
    194 
    195     setPressed(true);
    196 
    197     // Stay consistent with performClick() by sending the event after
    198     // setting the pressed state but before performing the action.
    199     sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    200 
    201     setPressed(false);
    202   }
    203 
    204   private void setLongHovered(boolean enabled) {
    205     if (longHovered != enabled) {
    206       longHovered = enabled;
    207 
    208       // Switch between normal and alternate description, if available.
    209       if (enabled) {
    210         backupContentDesc = getContentDescription();
    211         super.setContentDescription(longHoverContentDesc);
    212       } else {
    213         super.setContentDescription(backupContentDesc);
    214       }
    215     }
    216   }
    217 
    218   private void cancelLongHover() {
    219     if (longHoverRunnable != null) {
    220       removeCallbacks(longHoverRunnable);
    221     }
    222     setLongHovered(false);
    223   }
    224 
    225   public interface OnPressedListener {
    226 
    227     void onPressed(View view, boolean pressed);
    228   }
    229 }
    230