/*
* Copyright (C) 2011 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.speechrules;
import android.content.Context;
import android.os.Build;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityWindowInfo;
import com.android.talkback.InputModeManager;
import com.android.talkback.KeyComboManager;
import com.android.talkback.R;
import com.android.talkback.SpeechCleanupUtils;
import com.android.talkback.controller.CursorController;
import com.android.utils.Role;
import com.android.utils.StringBuilderUtils;
import com.android.utils.WindowManager;
import com.android.utils.compat.provider.SettingsCompatUtils;
import com.google.android.marvin.talkback.TalkBackService;
import java.util.List;
/**
* Processes editable text fields.
*/
class RuleEditText implements NodeSpeechRule, NodeHintRule {
@Override
public boolean accept(AccessibilityNodeInfoCompat node, AccessibilityEvent event) {
return Role.getRole(node) == Role.ROLE_EDIT_TEXT;
}
@Override
public CharSequence format(Context context, AccessibilityNodeInfoCompat node,
AccessibilityEvent event) {
final CharSequence text = getText(context, node);
boolean isCurrentlyEditing = node.isFocused();
if (hasWindowSupport()) {
isCurrentlyEditing = isCurrentlyEditing && isInputWindowOnScreen();
}
SpannableStringBuilder output = new SpannableStringBuilder();
CharSequence roleText = Role.getRoleDescriptionOrDefault(context, node);
StringBuilderUtils.append(output, roleText);
if (isCurrentlyEditing) {
CharSequence editing = context.getString(R.string.value_edit_box_editing);
StringBuilderUtils.append(output, editing);
}
if (TalkBackService.getInstance() != null
&& TalkBackService.getInstance().getCursorController().isSelectionModeActive()) {
StringBuilderUtils.append(output,
context.getString(R.string.notification_type_selection_mode_on));
}
StringBuilderUtils.append(output, text);
return output;
}
// package visibility for tests
boolean isInputWindowOnScreen() {
TalkBackService service = TalkBackService.getInstance();
if (service == null) {
return false;
}
WindowManager windowManager = new WindowManager(service.isScreenLayoutRTL());
List<AccessibilityWindowInfo> windows = service.getWindows();
windowManager.setWindows(windows);
return windowManager.isInputWindowOnScreen();
}
// package visibility for tests
boolean hasWindowSupport() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1;
}
@Override
public CharSequence getHintText(Context context, AccessibilityNodeInfoCompat node) {
int inputMode = InputModeManager.INPUT_MODE_TOUCH;
KeyComboManager keyComboManager = null;
// If the EditText already has the input focus, then we should not tell the user "double-tap
// to activate", nor "double-tap and hold to long press".
boolean skipClickHints = false;
TalkBackService service = TalkBackService.getInstance();
if (service != null) {
CursorController cursorController = service.getCursorController();
AccessibilityNodeInfoCompat cursor = cursorController.getCursorOrInputCursor();
if (cursor != null && cursor.isFocused() && node.equals(cursor)) {
skipClickHints = true;
}
inputMode = service.getInputModeManager().getInputMode();
keyComboManager = service.getKeyComboManager();
}
final CharSequence customClickHint = NodeHintHelper.getHintForInputMode(context, inputMode,
keyComboManager, context.getString(R.string.keycombo_shortcut_perform_click),
R.string.template_hint_edit_text, R.string.template_hint_edit_text_keyboard,
null /* label */);
final CharSequence customHint = NodeHintHelper.getCustomHintString(context, node,
customClickHint, null /* customLongClickHint */, skipClickHints, inputMode,
keyComboManager);
return customHint;
}
/**
* Inverts the default priorities of text and content description.
* If the field is a password, returns the content description or "password",
* as well as the length of the password if it's not empty.
*
* @param context current context
* @param node to get text from
* @return A text description of the editable text area.
*/
private CharSequence getText(Context context, AccessibilityNodeInfoCompat node) {
final CharSequence text = node.getText();
final boolean shouldSpeakPasswords = SettingsCompatUtils.SecureCompatUtils.shouldSpeakPasswords(context);
if (!TextUtils.isEmpty(text) && (!node.isPassword() || shouldSpeakPasswords)) {
// Text is potentially user input, so we need to make sure we pronounce input that has
// only symbols.
return SpeechCleanupUtils.collapseRepeatedCharactersAndCleanUp(context, text);
}
SpannableStringBuilder output = new SpannableStringBuilder();
final CharSequence contentDescription = node.getContentDescription();
if (!TextUtils.isEmpty(contentDescription)) {
// Less likely, but contentDescription is potentially user input, so we need to make
// sure we pronounce input that has only symbols.
StringBuilderUtils.append(output,
SpeechCleanupUtils.collapseRepeatedCharactersAndCleanUp(
context,
contentDescription));
} else if (node.isPassword() && !shouldSpeakPasswords) {
StringBuilderUtils.append(output, context.getString(R.string.value_password));
}
if (node.isPassword() && !shouldSpeakPasswords && !TextUtils.isEmpty(text)) {
// Note: never cleanup password speech because that will mess up the text length.
StringBuilderUtils.append(output, context.getResources().getQuantityString(
R.plurals.template_password_character_count,
text.length(),
text.length()));
}
return output;
}
}