/*
* Copyright (C) 2012 Google Inc.
*
* 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.speechrules;
import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.view.accessibility.AccessibilityEvent;
import com.android.talkback.InputModeManager;
import com.android.talkback.KeyComboManager;
import com.android.talkback.R;
import com.android.talkback.keyboard.KeyComboModel;
import com.android.utils.AccessibilityNodeInfoUtils;
import com.android.utils.StringBuilderUtils;
import com.google.android.marvin.talkback.TalkBackService;
import java.util.List;
public interface NodeHintRule {
/**
* Determines whether this rule should process the specified node.
*
* @param node The node to filter.
* @return {@code true} if this rule should process the node.
*/
boolean accept(AccessibilityNodeInfoCompat node, AccessibilityEvent event);
/**
* Processes the specified node and returns hint text to speak, or {@code null} if the node
* should not be spoken.
*
* @param context The parent context.
* @param node The node to process.
* @return A spoken hint description, or {@code null} if the node should not be spoken.
*/
CharSequence getHintText(Context context, AccessibilityNodeInfoCompat node);
class NodeHintHelper {
private static int sActionResId;
private static boolean sAllowLongClick;
private static boolean sAllowCustomActions;
static {
updateHints(false /* singleTap */, false /* television */);
}
public static void updateHints(boolean forceSingleTap, boolean isTelevision) {
if (isTelevision) {
sAllowLongClick = false;
sAllowCustomActions = false;
sActionResId = R.string.value_press_select;
} else {
sAllowLongClick = true;
sAllowCustomActions = true;
if (forceSingleTap) {
sActionResId = R.string.value_single_tap;
} else {
sActionResId = R.string.value_double_tap;
}
}
}
/**
* Get hint string from each action's label
* @param context application context
* @param node node to be examined
* @return hint strings
*/
public static CharSequence getDefaultHintString(Context context,
AccessibilityNodeInfoCompat node) {
TalkBackService service = TalkBackService.getInstance();
if (service == null) {
// If TalkBackService is not available, falls back to touch operation.
return getCustomHintString(context, node, null, null, false,
InputModeManager.INPUT_MODE_TOUCH, null /* keyComboManager */);
} else {
return getCustomHintString(context, node, null, null, false,
service.getInputModeManager().getInputMode(), service.getKeyComboManager());
}
}
/**
* Get hint string from each action's label, overriding the default click and long-click
* action hints.
* @param context application context
* @param node node to be examined
* @param customClickHint the custom hint for the click (or check) action
* @param customLongClickHint the custom hint for the long-click action
* @param skipClickHints set to true if we should skip the click/long-click action hints.
* @return hint strings
*/
public static CharSequence getCustomHintString(Context context,
AccessibilityNodeInfoCompat node,
@Nullable CharSequence customClickHint,
@Nullable CharSequence customLongClickHint,
boolean skipClickHints,
int inputMode,
@Nullable KeyComboManager keyComboManager) {
final SpannableStringBuilder builder = new SpannableStringBuilder();
// Speak custom actions first, if available
if (sAllowCustomActions) {
List<AccessibilityActionCompat> customActions =
AccessibilityNodeInfoUtils.getCustomActions(node);
if (!customActions.isEmpty()) {
// TODO: Should describe how to get to custom actions
StringBuilderUtils.appendWithSeparator(builder,
NodeHintHelper.getHintString(context,
R.string.template_hint_custom_actions));
for (AccessibilityActionCompat action : customActions) {
StringBuilderUtils.appendWithSeparator(builder, action.getLabel());
}
}
}
// Get hints from available action's label
final List<AccessibilityNodeInfoCompat.AccessibilityActionCompat> actions
= node.getActionList();
boolean hasClickActionHint = false;
boolean hasLongClickActionHint = false;
if (actions != null && !actions.isEmpty()) {
for (AccessibilityNodeInfoCompat.AccessibilityActionCompat action : actions) {
if (!AccessibilityNodeInfoUtils.isCustomAction(action) &&
!TextUtils.isEmpty(action.getLabel())) {
switch (action.getId()) {
case AccessibilityNodeInfoCompat.ACTION_CLICK:
hasClickActionHint = true;
if (!skipClickHints) {
StringBuilderUtils.appendWithSeparator(builder,
getHintForInputMode(context, inputMode,
keyComboManager,
context.getString(
R.string.keycombo_shortcut_perform_click),
R.string.template_custom_hint_for_actions,
R.string.template_custom_hint_for_actions_keyboard,
action.getLabel()));
}
break;
case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK:
if (sAllowLongClick) {
hasLongClickActionHint = true;
if (!skipClickHints) {
int longClickShortcutId = R.string.
keycombo_shortcut_perform_long_click;
int longClickHintIdForTouch = R.string.
template_custom_hint_for_long_clickable_actions;
int longClickHintIdForKeyboard = R.string.
template_custom_hint_for_actions_keyboard;
StringBuilderUtils.appendWithSeparator(builder,
getHintForInputMode(context, inputMode,
keyComboManager,
context.getString(longClickShortcutId),
longClickHintIdForTouch,
longClickHintIdForKeyboard,
action.getLabel()));
}
}
break;
default:
StringBuilderUtils.appendWithSeparator(builder, action.getLabel());
break;
}
}
}
}
if (skipClickHints) {
return builder;
}
boolean checkable = node.isCheckable();
boolean clickable = AccessibilityNodeInfoUtils.isClickable(node);
boolean longClickable = AccessibilityNodeInfoUtils.isLongClickable(node);
// Add a click hint if there's a click action but no corresponding label.
if ((clickable || checkable) && !hasClickActionHint) {
// If a custom click hint is provided, use that; otherwise choose a default hint
// depending on whether we have a checkable node or not.
if (!TextUtils.isEmpty(customClickHint)) {
StringBuilderUtils.appendWithSeparator(builder, customClickHint);
} else if (checkable) {
StringBuilderUtils.appendWithSeparator(builder,
getHintForInputMode(context, inputMode, keyComboManager,
context.getString(R.string.keycombo_shortcut_perform_click),
R.string.template_hint_checkable,
R.string.template_hint_checkable_keyboard,
null /* label */));
} else {
StringBuilderUtils.appendWithSeparator(builder,
getHintForInputMode(context, inputMode, keyComboManager,
context.getString(R.string.keycombo_shortcut_perform_click),
R.string.template_hint_clickable,
R.string.template_hint_clickable_keyboard,
null /* label */));
}
}
// Add a long click hint if there's a long-click action but no corresponding label.
if (sAllowLongClick && longClickable && !hasLongClickActionHint) {
// If a custom long-click hint is provided, use that; otherwise use default.
if (!TextUtils.isEmpty(customLongClickHint)) {
StringBuilderUtils.appendWithSeparator(builder, customLongClickHint);
} else {
StringBuilderUtils.appendWithSeparator(builder,
getHintForInputMode(context, inputMode, keyComboManager,
context.getString(R.string.keycombo_shortcut_perform_long_click),
R.string.template_hint_long_clickable,
R.string.template_hint_long_clickable_keyboard,
null /* label */));
}
}
return builder;
}
// TODO: make this method private and provide different interface to callers of this
// method, i.e. refactor interfaces of this class considering keyboard based navigation
// hints.
public static CharSequence getHintForInputMode(
Context context, int inputMode, @Nullable KeyComboManager keyComboManager,
String keyboardShortcutKey, int templateResourceIdForTouch,
int templateResourceIdForKeyboard, @Nullable CharSequence label) {
// If keyCombo is not available (e.g. no key combo is assigned, keyComboManager is not
// provided), falls back to touch operation hint.
boolean doesProvideKeyboardHint = inputMode == InputModeManager.INPUT_MODE_KEYBOARD &&
keyComboManager != null &&
keyComboManager.getKeyComboModel().getKeyComboCodeForKey(keyboardShortcutKey) !=
KeyComboModel.KEY_COMBO_CODE_UNASSIGNED;
String action;
int templateResourceId;
if (doesProvideKeyboardHint) {
KeyComboModel keyComboModel = keyComboManager.getKeyComboModel();
long keyComboCode = keyComboModel.getKeyComboCodeForKey(keyboardShortcutKey);
long keyComboCodeWithTriggerModifier = KeyComboManager.getKeyComboCode(
KeyComboManager.getModifier(keyComboCode) |
keyComboModel.getTriggerModifier(),
KeyComboManager.getKeyCode(keyComboCode));
action = keyComboManager.getKeyComboStringRepresentation(
keyComboCodeWithTriggerModifier);
templateResourceId = templateResourceIdForKeyboard;
} else {
action = context.getString(sActionResId);
templateResourceId = templateResourceIdForTouch;
}
if (label == null) {
return context.getString(templateResourceId, action);
} else {
return context.getString(templateResourceId, action, label);
}
}
/**
* Returns a hint string populated with the version-appropriate action
* string.
*
* @param context The parent context.
* @param hintResId The hint string's resource identifier.
* @return A populated hint string.
*/
public static CharSequence getHintString(Context context, int hintResId) {
return context.getString(hintResId, context.getString(sActionResId));
}
}
}