/*
* 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.menurules;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import com.android.talkback.EditTextActionHistory;
import com.android.talkback.R;
import com.android.talkback.SpeechCleanupUtils;
import com.android.talkback.SpeechController;
import com.android.talkback.controller.TextCursorController;
import com.android.utils.Role;
import com.google.android.marvin.talkback.TalkBackService;
import com.android.talkback.contextmenu.ContextMenuItem;
import com.android.talkback.contextmenu.ContextMenuItemBuilder;
import com.android.talkback.controller.CursorController;
import com.android.talkback.controller.FeedbackController;
import com.android.utils.AccessibilityNodeInfoUtils;
import com.android.utils.PerformActionUtils;
import java.util.LinkedList;
import java.util.List;
/**
* Processes editable text fields.
*/
class RuleEditText implements NodeMenuRule {
/**
* Default pitch adjustment for text copy event feedback.
*/
private static final float DEFAULT_COPY_PITCH = 1.2f;
@Override
public boolean accept(TalkBackService service, AccessibilityNodeInfoCompat node) {
return Role.getRole(node) == Role.ROLE_EDIT_TEXT;
}
@Override
public List<ContextMenuItem> getMenuItemsForNode(
TalkBackService service, ContextMenuItemBuilder menuItemBuilder,
AccessibilityNodeInfoCompat node) {
final AccessibilityNodeInfoCompat nodeCopy = AccessibilityNodeInfoCompat.obtain(node);
final CursorController cursorController = service.getCursorController();
final List<ContextMenuItem> items = new LinkedList<>();
// This action has inconsistencies with EditText nodes that have
// contentDescription attributes.
if (TextUtils.isEmpty(nodeCopy.getContentDescription())) {
if (AccessibilityNodeInfoUtils.supportsAnyAction(nodeCopy,
AccessibilityNodeInfoCompat.ACTION_SET_SELECTION,
AccessibilityNodeInfoCompat.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY)) {
ContextMenuItem moveToBeginning = menuItemBuilder.createMenuItem(service,
Menu.NONE, R.id.edittext_breakout_move_to_beginning,
Menu.NONE,
service.getString(R.string.title_edittext_breakout_move_to_beginning));
moveToBeginning.setSkipRefocusEvents(true);
items.add(moveToBeginning);
}
if (AccessibilityNodeInfoUtils.supportsAnyAction(nodeCopy,
AccessibilityNodeInfoCompat.ACTION_SET_SELECTION,
AccessibilityNodeInfoCompat.ACTION_NEXT_AT_MOVEMENT_GRANULARITY)) {
ContextMenuItem moveToEnd = menuItemBuilder.createMenuItem(service,
Menu.NONE, R.id.edittext_breakout_move_to_end,
Menu.NONE,
service.getString(R.string.title_edittext_breakout_move_to_end));
moveToEnd.setSkipRefocusEvents(true);
items.add(moveToEnd);
}
if (AccessibilityNodeInfoUtils.supportsAnyAction(
nodeCopy, AccessibilityNodeInfoCompat.ACTION_CUT)) {
ContextMenuItem cut = menuItemBuilder.createMenuItem(service,
Menu.NONE, R.id.edittext_breakout_cut,
Menu.NONE,
service.getString(android.R.string.cut));
cut.setSkipRefocusEvents(true);
items.add(cut);
}
if (AccessibilityNodeInfoUtils.supportsAnyAction(
nodeCopy, AccessibilityNodeInfoCompat.ACTION_COPY)) {
ContextMenuItem copy = menuItemBuilder.createMenuItem(service,
Menu.NONE, R.id.edittext_breakout_copy,
Menu.NONE,
service.getString(android.R.string.copy));
copy.setSkipRefocusEvents(true);
items.add(copy);
}
if (AccessibilityNodeInfoUtils.supportsAnyAction(
nodeCopy, AccessibilityNodeInfoCompat.ACTION_PASTE)) {
ContextMenuItem paste = menuItemBuilder.createMenuItem(service,
Menu.NONE, R.id.edittext_breakout_paste,
Menu.NONE,
service.getString(android.R.string.paste));
paste.setSkipRefocusEvents(true);
items.add(paste);
}
if (AccessibilityNodeInfoUtils.supportsAnyAction(
nodeCopy, AccessibilityNodeInfoCompat.ACTION_SET_SELECTION) &&
nodeCopy.getText() != null) {
ContextMenuItem select = menuItemBuilder.createMenuItem(service,
Menu.NONE, R.id.edittext_breakout_select_all,
Menu.NONE,
service.getString(android.R.string.selectAll));
select.setSkipRefocusEvents(true);
items.add(select);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
// Text selection APIs are available in API 18+
// TODO Use a checkable menu item once supported.
final ContextMenuItem selectionMode;
if (cursorController.isSelectionModeActive()) {
selectionMode = menuItemBuilder.createMenuItem(service,
Menu.NONE, R.id.edittext_breakout_end_selection_mode,
Menu.NONE,
service.getString(R.string.title_edittext_breakout_end_selection_mode));
} else {
selectionMode = menuItemBuilder.createMenuItem(service,
Menu.NONE, R.id.edittext_breakout_start_selection_mode,
Menu.NONE,
service.getString(R.string.title_edittext_breakout_start_selection_mode));
}
selectionMode.setSkipRefocusEvents(true);
items.add(selectionMode);
}
}
for (ContextMenuItem item : items) {
item.setOnMenuItemClickListener(new EditTextMenuItemClickListener(service, nodeCopy));
}
return items;
}
@Override
public CharSequence getUserFriendlyMenuName(Context context) {
return context.getString(R.string.title_edittext_controls);
}
@Override
public boolean canCollapseMenu() {
return true;
}
private static class EditTextMenuItemClickListener implements MenuItem.OnMenuItemClickListener {
private final TalkBackService mService;
private final FeedbackController mFeedback;
private final CursorController mCursorController;
private final TextCursorController mTextCursorController;
private final ClipboardManager mClipboardManager;
private final SpeechController mSpeechController;
private final AccessibilityNodeInfoCompat mNode;
public EditTextMenuItemClickListener(
TalkBackService service, AccessibilityNodeInfoCompat node) {
mService = service;
mFeedback = service.getFeedbackController();
mCursorController = service.getCursorController();
mTextCursorController = service.getTextCursorController();
mClipboardManager = (ClipboardManager) service.getSystemService(
Context.CLIPBOARD_SERVICE);
mSpeechController = service.getSpeechController();
mNode = node;
}
@Override
public boolean onMenuItemClick(MenuItem item) {
if (item == null) {
mNode.recycle();
return true;
}
final int itemId = item.getItemId();
final Bundle args = new Bundle();
final boolean result;
if (itemId == R.id.edittext_breakout_move_to_beginning) {
mTextCursorController.forceSetCursorPosition(0, 0);
if (AccessibilityNodeInfoUtils.supportsAction(mNode,
AccessibilityNodeInfoCompat.ACTION_SET_SELECTION)) {
args.putInt(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SELECTION_START_INT, 0);
args.putInt(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SELECTION_END_INT, 0);
result = PerformActionUtils.performAction(mNode,
AccessibilityNodeInfoCompat.ACTION_SET_SELECTION, args);
} else {
args.putInt(
AccessibilityNodeInfoCompat.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_PAGE);
result = PerformActionUtils.performAction(mNode,
AccessibilityNodeInfoCompat.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
args);
}
mSpeechController.speak(
mService.getString(R.string.notification_type_beginning_of_field),
/** It makes sense to interrupt all the previous utterances generated in
* the local context menu. After the cursor action is performed, it's
* the most important to notify the user what happens to the edit text. */
SpeechController.QUEUE_MODE_INTERRUPT,
0,
null);
} else if (itemId == R.id.edittext_breakout_move_to_end) {
int length = 0;
if (mNode.getText() != null) {
length = mNode.getText().length();
mTextCursorController.forceSetCursorPosition(length, length);
}
if (AccessibilityNodeInfoUtils.supportsAction(mNode,
AccessibilityNodeInfoCompat.ACTION_SET_SELECTION) &&
mNode.getText() != null) {
args.putInt(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SELECTION_START_INT,
length);
args.putInt(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SELECTION_END_INT,
length);
result = PerformActionUtils.performAction(mNode,
AccessibilityNodeInfoCompat.ACTION_SET_SELECTION, args);
} else {
args.putInt(
AccessibilityNodeInfoCompat.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_PAGE);
result = PerformActionUtils.performAction(mNode,
AccessibilityNodeInfoCompat.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, args);
}
mSpeechController.speak(
mService.getString(R.string.notification_type_end_of_field),
SpeechController.QUEUE_MODE_INTERRUPT,
0,
null);
} else if (itemId == R.id.edittext_breakout_cut) {
EditTextActionHistory.getInstance().beforeCut();
result = PerformActionUtils.performAction(mNode,
AccessibilityNodeInfoCompat.ACTION_CUT);
EditTextActionHistory.getInstance().afterCut();
} else if (itemId == R.id.edittext_breakout_copy) {
result = PerformActionUtils.performAction(mNode,
AccessibilityNodeInfoCompat.ACTION_COPY);
ClipData data = mClipboardManager.getPrimaryClip();
if (data != null && data.getItemCount() > 0
&& data.getItemAt(0).getText() != null) {
Bundle params = new Bundle();
params.putFloat(SpeechController.SpeechParam.PITCH, DEFAULT_COPY_PITCH);
mSpeechController.speak(
mService.getString(R.string.template_text_copied,
data.getItemAt(0).getText().toString()),
SpeechController.QUEUE_MODE_INTERRUPT,
0,
params);
}
} else if (itemId == R.id.edittext_breakout_paste) {
EditTextActionHistory.getInstance().beforePaste();
result = PerformActionUtils.performAction(mNode,
AccessibilityNodeInfoCompat.ACTION_PASTE);
EditTextActionHistory.getInstance().afterPaste();
} else if (itemId == R.id.edittext_breakout_select_all && mNode.getText() != null) {
EditTextActionHistory.getInstance().beforeSelectAll();
args.putInt(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SELECTION_START_INT, 0);
args.putInt(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SELECTION_END_INT,
mNode.getText().length());
result = PerformActionUtils.performAction(mNode,
AccessibilityNodeInfoCompat.ACTION_SET_SELECTION, args);
EditTextActionHistory.getInstance().afterSelectAll();
mSpeechController.speak(
SpeechCleanupUtils.cleanUp(mService,
mService.getString(R.string.template_announce_selected_text,
mNode.getText())),
SpeechController.QUEUE_MODE_INTERRUPT,
0,
null);
} else if (itemId == R.id.edittext_breakout_start_selection_mode) {
mCursorController.setSelectionModeActive(mNode, true);
result = true;
mSpeechController.speak(
mService.getString(R.string.notification_type_selection_mode_on),
SpeechController.QUEUE_MODE_INTERRUPT,
0,
null);
} else if (itemId == R.id.edittext_breakout_end_selection_mode) {
mCursorController.setSelectionModeActive(mNode, false);
result = true;
mSpeechController.speak(
mService.getString(R.string.notification_type_selection_mode_off),
SpeechController.QUEUE_MODE_INTERRUPT,
0,
null);
int start = mNode.getTextSelectionStart();
int end = mNode.getTextSelectionEnd();
if (start > end) {
int tmp = start;
start = end;
end = tmp;
}
CharSequence text = mNode.getText();
if (text != null && start >= 0 && start <= text.length()
&& end >= 0 && end <= text.length()) {
CharSequence textToSpeak;
if (start != end) {
textToSpeak = mService.getString(R.string.template_announce_selected_text,
text.subSequence(start, end));
} else {
textToSpeak = mService.getString(R.string.template_no_text_selected);
}
mSpeechController.speak(
textToSpeak,
SpeechController.QUEUE_MODE_QUEUE,
0,
null);
}
} else {
result = false;
}
if (result) {
TalkBackService service = TalkBackService.getInstance();
if (service != null) {
service.getAnalytics().onTextEdited();
}
} else {
mFeedback.playAuditory(R.raw.complete);
}
mNode.recycle();
return true;
}
}
}