/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.android.talkback.formatter; import android.os.Build; import android.support.v4.view.accessibility.AccessibilityEventCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.support.v4.view.accessibility.AccessibilityRecordCompat; import android.text.TextUtils; import android.view.accessibility.AccessibilityEvent; import com.android.talkback.R; import com.android.utils.Role; import com.google.android.marvin.talkback.TalkBackService; import com.android.talkback.Utterance; import com.android.utils.AccessibilityEventUtils; /** * Filters and formats click events from checkable items. * * @deprecated Only use this class for legacy support in KitKat or earlier. For new development * targeting Lollipop or later, please use the {@link ClickFormatter} instead. */ @Deprecated public class CheckableClickedFormatter implements EventSpeechRule.AccessibilityEventFilter, EventSpeechRule.AccessibilityEventFormatter { private long mClickedTime = -1; private AccessibilityNodeInfoCompat mClickedNode; // accept and format are not called from the same class instance private static AccessibilityNodeInfoCompat sCachedCheckableNode; private boolean findClickedCheckableNode(AccessibilityNodeInfoCompat source) { if (source == null) return false; if (source.equals(mClickedNode)) { boolean ret = findCheckableNode(source); if (mClickedNode != null) { mClickedNode.recycle(); mClickedNode = null; } mClickedTime = -1; return ret; } int children = source.getChildCount(); for (int i = 0; i < children; i++) { AccessibilityNodeInfoCompat node = source.getChild(i); if (findClickedCheckableNode(node)) { if (!sCachedCheckableNode.equals(node)) { node.recycle(); } return true; } if (node != null) { node.recycle(); } } return false; } private boolean findCheckableNode(AccessibilityNodeInfoCompat source) { if (source == null) { return false; } if (source.isCheckable()) { sCachedCheckableNode = source; return true; } int children = source.getChildCount(); for (int i = 0; i < children; i++) { AccessibilityNodeInfoCompat node = source.getChild(i); if (findCheckableNode(node)) { if (!sCachedCheckableNode.equals(node)) { node.recycle(); } return true; } if (node != null) { node.recycle(); } } return false; } @Override public boolean accept(AccessibilityEvent event, TalkBackService context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) return false; int type = event.getEventType(); if (type == AccessibilityEvent.TYPE_VIEW_CLICKED) { mClickedNode = null; mClickedTime = -1; if (event.isChecked()) { return true; } AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event); AccessibilityNodeInfoCompat source = record.getSource(); if (source != null) { if (source.isCheckable()) { return true; } // it is bug in settings application that does not include clicked state on node // so we need to restore it later from TYPE_WINDOW_CONTENT_CHANGED event if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mClickedNode = source; mClickedTime = System.currentTimeMillis(); } } return false; } if (type == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return false; if (mClickedTime == -1 || mClickedNode == null) return false; long now = System.currentTimeMillis(); if ((mClickedTime + 1000) < now) { mClickedTime = -1; if (mClickedNode != null) { mClickedNode.recycle(); mClickedNode = null; } return false; } AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event); AccessibilityNodeInfoCompat source = record.getSource(); return findClickedCheckableNode(source); } return false; } @Override public boolean format(AccessibilityEvent event, TalkBackService context, Utterance utterance) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { if (sCachedCheckableNode == null) return false; // Need to get latest state of cached node before accessing. sCachedCheckableNode.refresh(); utterance.addAuditory(R.raw.tick); utterance.addHaptic(R.array.view_clicked_pattern); utterance.addSpoken(context.getString(sCachedCheckableNode.isChecked() ? R.string.value_checked : R.string.value_not_checked)); sCachedCheckableNode.recycle(); sCachedCheckableNode = null; return true; } AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event); AccessibilityNodeInfoCompat source = record.getSource(); utterance.addAuditory(R.raw.tick); utterance.addHaptic(R.array.view_clicked_pattern); CharSequence eventText = AccessibilityEventUtils.getEventTextOrDescription(event); if (!TextUtils.isEmpty(eventText)) { utterance.addSpoken(eventText); } // Switch and ToggleButton state is sent along with the event, so only // append checked / not checked state for other types of controls. // TODO: node.isTwoState() if (Role.getRole(source) == Role.ROLE_TOGGLE_BUTTON || Role.getRole(source) == Role.ROLE_SWITCH) { return true; } if (source == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { if (event.isChecked()) { utterance.addSpoken(context.getString(R.string.value_checked)); } else { utterance.addSpoken(context.getString(R.string.value_not_checked)); } return true; } if (source.isCheckable()) { if (source.isChecked()) { utterance.addSpoken(context.getString(R.string.value_checked)); } else { utterance.addSpoken(context.getString(R.string.value_not_checked)); } } return true; } }