Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions differences.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,16 @@ TalkBack echoed a word such as "don't" as two words. Backtalk echoes it as one w

When you turn Backtalk off, it says "Backtalk off" at the accessibility volume, using your device's real volume levels. TalkBack only approximated that volume.

### Table column headers

In **Verbosity**, you can choose whether Backtalk reads table column headers before or after the cell's data, or leaves them out:

* **After cell data**, the default, keeps TalkBack's order: the cell's contents, then the row and column headers or numbers.
* **Before cell data** speaks the row and column headers or numbers before the cell's contents.
* **Do not read** leaves column headers out, and reads column numbers only when row and column numbers are on.

Under preset settings in **Verbosity**, you can also turn off **Speak row and column numbers** to hear only named headers without row and column coordinates. The setting is on by default.

## Notifications

### Speak notifications setting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3393,6 +3393,24 @@ protected void reloadPreferences(@Nullable String key) {
res.getBoolean(R.bool.pref_speak_container_element_positions_default));
globalVariables.setSpeakCollectionInfo(speakCollectionInfo);

// Update preference: table column headers order/visibility.
String tableColumnHeaders =
SharedPreferencesUtils.getStringPref(
prefs,
res,
R.string.pref_table_column_headers_key,
R.string.pref_table_column_headers_default);
globalVariables.setTableColumnHeaders(tableColumnHeaders);

// Update preference: speak table row and column numbers.
boolean speakTableRowColNumbers =
VerbosityPreferences.getPreferenceValueBool(
prefs,
res,
res.getString(R.string.pref_table_speak_row_column_numbers_key),
res.getBoolean(R.bool.pref_table_speak_row_column_numbers_default));
globalVariables.setSpeakTableRowColumnNumbers(speakTableRowColNumbers);

// Update preference: speak roles.
boolean speakRoles =
VerbosityPreferences.getPreferenceValueBool(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,24 @@ public static CharSequence getCollectionItemTransitionDescription(
@Nullable AccessibilityNodeInfoCompat focusedNode,
CollectionState collectionState,
Context context) {
return getCollectionItemTransitionDescription(
focusedNode,
collectionState,
context,
GlobalVariables.TABLE_HEADERS_AFTER,
/* speakTableRowColumnNumbers= */ true);
}

/**
* Returns the collection item description when the collection is transitioned, with table header
* and coordinate preferences applied.
*/
public static CharSequence getCollectionItemTransitionDescription(
@Nullable AccessibilityNodeInfoCompat focusedNode,
CollectionState collectionState,
Context context,
String tableColumnHeaders,
boolean speakTableRowColumnNumbers) {
boolean isRowTransition = getCollectionIsRowTransition(collectionState);
boolean isColumnTransition = getCollectionIsColumnTransition(collectionState);
if (isRowTransition || isColumnTransition) {
Expand All @@ -229,16 +247,28 @@ public static CharSequence getCollectionItemTransitionDescription(
if (isRowTransition
&& tableItemRowIndex != -1
&& headingType != CollectionState.TYPE_ROW) {
joinList.add(
getCollectionTableItemRowName(collectionState, tableItemRowIndex, context));
CharSequence rowDesc =
getCollectionTableItemRowName(
collectionState, tableItemRowIndex, context, speakTableRowColumnNumbers);
if (!TextUtils.isEmpty(rowDesc)) {
joinList.add(rowDesc);
}
}

int tableItemColumnIndex = getCollectionTableItemColumnIndex(collectionState);
if (isColumnTransition
&& tableItemColumnIndex != -1
&& headingType != CollectionState.TYPE_COLUMN) {
joinList.add(
getCollectionTableItemColumnName(collectionState, tableItemColumnIndex, context));
CharSequence colDesc =
getCollectionTableItemColumnName(
collectionState,
tableItemColumnIndex,
context,
tableColumnHeaders,
speakTableRowColumnNumbers);
if (!TextUtils.isEmpty(colDesc)) {
joinList.add(colDesc);
}
}
return CompositorUtils.joinCharSequences(joinList, CompositorUtils.getSeparator(), true);
}
Expand Down Expand Up @@ -321,32 +351,58 @@ private static CharSequence getCollectionTableItemRowName(CollectionState collec
}

private static CharSequence getCollectionTableItemRowName(
CollectionState collectionState, int tableItemRowIndex, Context context) {
CollectionState collectionState,
int tableItemRowIndex,
Context context,
boolean speakTableRowColumnNumbers) {
CharSequence tableItemRowName = getCollectionTableItemRowName(collectionState);
if (!TextUtils.isEmpty(tableItemRowName)) {
return tableItemRowName;
} else {
} else if (speakTableRowColumnNumbers) {
int newRowIndex = tableItemRowIndex + 1;
return context.getString(R.string.row_index_template, newRowIndex);
} else {
return "";
}
}

private static CharSequence getCollectionTableItemRowName(
CollectionState collectionState, int tableItemRowIndex, Context context) {
return getCollectionTableItemRowName(collectionState, tableItemRowIndex, context, true);
}

private static CharSequence getCollectionTableItemColumnName(CollectionState collectionState) {
CollectionState.TableItemState itemState = collectionState.getTableItemState();
return itemState != null ? itemState.getColumnName() : "";
return (collectionState != null && collectionState.getTableItemState() != null)
? collectionState.getTableItemState().getColumnName()
: "";
}

private static CharSequence getCollectionTableItemColumnName(
CollectionState collectionState, int tableItemColumnIndex, Context context) {
CharSequence tableItemColumnName = getCollectionTableItemColumnName(collectionState);
CollectionState collectionState,
int tableItemColumnIndex,
Context context,
String tableColumnHeaders,
boolean speakTableRowColumnNumbers) {
CharSequence tableItemColumnName =
GlobalVariables.TABLE_HEADERS_OFF.equals(tableColumnHeaders)
? ""
: getCollectionTableItemColumnName(collectionState);
if (!TextUtils.isEmpty(tableItemColumnName)) {
return tableItemColumnName;
} else {
} else if (speakTableRowColumnNumbers) {
int newColumnIndex = tableItemColumnIndex + 1;
return context.getString(R.string.column_index_template, newColumnIndex);
} else {
return "";
}
}

private static CharSequence getCollectionTableItemColumnName(
CollectionState collectionState, int tableItemColumnIndex, Context context) {
return getCollectionTableItemColumnName(
collectionState, tableItemColumnIndex, context, GlobalVariables.TABLE_HEADERS_AFTER, true);
}

private static CharSequence getCollectionTableItemRoleDescription(
CollectionState collectionState) {
CollectionState.TableItemState itemState = collectionState.getTableItemState();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import com.google.android.accessibility.utils.FormFactorUtils;
import com.google.android.accessibility.utils.KeyboardUtils;
import com.google.android.accessibility.utils.Logger;
import com.google.android.accessibility.utils.Role;
import com.google.android.accessibility.utils.TimedFlags;
import com.google.android.accessibility.utils.input.WindowsDelegate;
import com.google.android.accessibility.utils.monitor.CollectionState;
Expand Down Expand Up @@ -179,6 +180,13 @@ public class GlobalVariables extends TimedFlags implements ParseTree.VariableDel
private boolean speakRoles = true;
private boolean speakCollectionInfo = true;

public static final String TABLE_HEADERS_BEFORE = "before";
public static final String TABLE_HEADERS_AFTER = "after";
public static final String TABLE_HEADERS_OFF = "off";

private String tableColumnHeaders = TABLE_HEADERS_AFTER;
private boolean speakTableRowColumnNumbers = true;

// Control sounds: whether they are heard, and the ones heard or felt for focused controls.
private boolean controlSoundsOn = false;
private Set<Integer> controlSounds = Collections.emptySet();
Expand Down Expand Up @@ -610,6 +618,22 @@ public void setSpeakCollectionInfo(boolean value) {
speakCollectionInfo = value;
}

public String getTableColumnHeaders() {
return tableColumnHeaders;
}

public void setTableColumnHeaders(String value) {
tableColumnHeaders = value;
}

public boolean getSpeakTableRowColumnNumbers() {
return speakTableRowColumnNumbers;
}

public void setSpeakTableRowColumnNumbers(boolean value) {
speakTableRowColumnNumbers = value;
}

public boolean getSpeakRoles() {
return speakRoles;
}
Expand Down Expand Up @@ -901,7 +925,11 @@ public CharSequence getCollectionTransitionDescription() {
public CharSequence getCollectionItemTransitionDescription(
@Nullable AccessibilityNodeInfoCompat focusedNode) {
return CollectionStateFeedbackUtils.getCollectionItemTransitionDescription(
focusedNode, collectionState, mContext);
focusedNode, collectionState, mContext, tableColumnHeaders, speakTableRowColumnNumbers);
}

public int getCollectionRole() {
return collectionState.getCollectionRole();
}

/** Returns if the reading menu has actions settings. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,57 +258,77 @@ private static CharSequence viewAccessibilityFocusedDescription(
CharSequence eventDescription =
AccessibilityEventFeedbackUtils.getEventContentDescriptionOrEventAggregateText(
event, preferredLocale);
CharSequence contentDescription;
if (!TextUtils.isEmpty(nodeUnlabelledState)) {
CharSequence unlabelledDescription =
TextUtils.isEmpty(eventDescription) ? nodeUnlabelledState : eventDescription;
outputJoinList.add(unlabelledDescription);
contentDescription = unlabelledDescription;
logString
.append(String.format("\n unlabelledDescription={%s}", unlabelledDescription))
.append(String.format(", eventDescription={%s}", eventDescription));
} else {
CharSequence nodeTreeDescription =
treeNodesDescription.aggregateNodeTreeDescription(node, event);
if (!TextUtils.isEmpty(nodeTreeDescription)) {
outputJoinList.add(nodeTreeDescription);
contentDescription = nodeTreeDescription;
logString.append(String.format("\n nodeTreeDescription={%s}", nodeTreeDescription));
} else {
outputJoinList.add(eventDescription);
contentDescription = eventDescription;
logString.append(String.format("\n eventDescription={%s}", eventDescription));
}
}

// Add phonetic spelling if necessary.
Optional<CharSequence> phoneticExample =
processorPhoneticLetters.getPhoneticLetterForKeyboardFocusEvent(event);
phoneticExample.ifPresent(outputJoinList::add);
logString.append(String.format("\n phoneticExample={%s}", phoneticExample));

// Prepare Collection item transition state or Node role/heading description for feedback.
boolean speakCollectionInfo = globalVariables.getSpeakCollectionInfo();
boolean speakRoles = globalVariables.getSpeakRoles();
logString
.append(String.format("\n Verbosity speakCollectionInfo=%s", speakCollectionInfo))
.append(String.format(", speakRoles=%s", speakRoles));
CharSequence collectionItemTransition =
speakCollectionInfo ? globalVariables.getCollectionItemTransitionDescription(node) : "";
if (!TextUtils.isEmpty(collectionItemTransition)) {

boolean isTableGrid = (globalVariables.getCollectionRole() == Role.ROLE_GRID);
boolean headersBefore =
isTableGrid
&& GlobalVariables.TABLE_HEADERS_BEFORE.equals(
globalVariables.getTableColumnHeaders());

if (headersBefore && !TextUtils.isEmpty(collectionItemTransition)) {
outputJoinList.add(collectionItemTransition);
logString.append(
String.format("\n collectionItemTransition={%s}", collectionItemTransition));
} else if (speakRoles
&& !WebInterfaceUtils.isWebContainer(node)
&& AccessibilityNodeInfoUtils.isHeading(node)) {
// If the source node has collection item transition, collectionItemTransition text would
// not be empty. And TalkBack should announce the collection item transition information or it
// should fallback to announce the role/heading description.
CharSequence nodeRoleDescription =
AccessibilityNodeFeedbackUtils.getNodeRoleDescription(node, context, globalVariables);
if (!TextUtils.isEmpty(nodeRoleDescription)) {
outputJoinList.add(nodeRoleDescription);
logString.append(String.format("\n nodeRoleDescription={%s}", nodeRoleDescription));
} else {
outputJoinList.add(context.getString(R.string.heading_template));
logString.append("\n heading");
}

if (!TextUtils.isEmpty(contentDescription)) {
outputJoinList.add(contentDescription);
}

// Add phonetic spelling if necessary.
Optional<CharSequence> phoneticExample =
processorPhoneticLetters.getPhoneticLetterForKeyboardFocusEvent(event);
phoneticExample.ifPresent(outputJoinList::add);
logString.append(String.format("\n phoneticExample={%s}", phoneticExample));

// Prepare Collection item transition state or Node role/heading description for feedback.
if (!headersBefore) {
if (!TextUtils.isEmpty(collectionItemTransition)) {
outputJoinList.add(collectionItemTransition);
logString.append(
String.format("\n collectionItemTransition={%s}", collectionItemTransition));
} else if (speakRoles
&& !WebInterfaceUtils.isWebContainer(node)
&& AccessibilityNodeInfoUtils.isHeading(node)) {
// If the source node has collection item transition, collectionItemTransition text would
// not be empty. And TalkBack should announce the collection item transition information or it
// should fallback to announce the role/heading description.
CharSequence nodeRoleDescription =
AccessibilityNodeFeedbackUtils.getNodeRoleDescription(node, context, globalVariables);
if (!TextUtils.isEmpty(nodeRoleDescription)) {
outputJoinList.add(nodeRoleDescription);
logString.append(String.format("\n nodeRoleDescription={%s}", nodeRoleDescription));
} else {
outputJoinList.add(context.getString(R.string.heading_template));
logString.append("\n heading");
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ private void buildMap() {
.put(
getString(R.string.pref_speak_container_element_positions_key),
getResources().getBoolean(R.bool.pref_speak_container_element_positions_default))
.put(
getString(R.string.pref_table_speak_row_column_numbers_key),
getResources().getBoolean(R.bool.pref_table_speak_row_column_numbers_default))
.put(
getString(R.string.pref_verbose_scroll_announcement_key),
getResources().getBoolean(R.bool.pref_verbose_scroll_announcement_default))
Expand Down
20 changes: 20 additions & 0 deletions talkback/src/main/res/values/donottranslate.xml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@
<string name="pref_capital_letters_key">pref_capital_letters</string>
<string name="pref_a11y_hints_key">pref_a11y_hints</string>
<string name="pref_speak_container_element_positions_key">pref_speak_container_element_positions_key</string>
<string name="pref_table_headers_category_key">pref_table_headers_category_key</string>
<string name="pref_table_column_headers_key">pref_table_column_headers_key</string>
<string name="pref_table_speak_row_column_numbers_key">pref_table_speak_row_column_numbers_key</string>
<string name="pref_speak_time_key">pref_speak_time_key</string>
<string name="pref_on_device_ai_key" translatable="false">pref_on_device_ai</string>
<string name="pref_speak_roles_key">pref_speak_roles_key</string>
Expand Down Expand Up @@ -730,6 +733,22 @@
<string name="pref_node_desc_order_value_role_name_state_pos">pref_node_desc_order_value_role_name_state_pos</string>
<string name="pref_node_desc_order_value_name_role_state_pos">pref_node_desc_order_value_name_role_state_pos</string>

<!-- Table column headers preferences -->
<string-array name="pref_table_column_headers_entries">
<item>@string/pref_table_column_headers_entry_after</item>
<item>@string/pref_table_column_headers_entry_before</item>
<item>@string/pref_table_column_headers_entry_off</item>
</string-array>
<string-array name="pref_table_column_headers_values">
<item>@string/pref_table_column_headers_value_after</item>
<item>@string/pref_table_column_headers_value_before</item>
<item>@string/pref_table_column_headers_value_off</item>
</string-array>
<string name="pref_table_column_headers_value_after">after</string>
<string name="pref_table_column_headers_value_before">before</string>
<string name="pref_table_column_headers_value_off">off</string>
<string name="pref_table_column_headers_default">@string/pref_table_column_headers_value_after</string>

<!-- Time feedback format -->
<string-array name="pref_time_feedback_format_entries">
<item>@string/pref_time_feedback_format_entries_default</item>
Expand Down Expand Up @@ -1406,6 +1425,7 @@
<bool name="pref_phonetic_letters_default">true</bool>
<bool name="pref_a11y_hints_default">true</bool>
<bool name="pref_speak_container_element_positions_default">true</bool>
<bool name="pref_table_speak_row_column_numbers_default">true</bool>
<bool name="pref_tell_time_default">true</bool>
<bool name="pref_speak_roles_default">true</bool>
<bool name="pref_speak_system_window_titles_default">true</bool>
Expand Down
9 changes: 9 additions & 0 deletions talkback/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,15 @@
-->
<string name="pref_speak_container_element_positions_title">Speak container info</string>

<!-- Table reading preferences in Verbosity settings -->
<string name="pref_table_headers_category_title" tools:ignore="MissingTranslation">Table reading</string>
<string name="pref_table_column_headers_title" tools:ignore="MissingTranslation">Read column headers</string>
<string name="pref_table_column_headers_entry_after" tools:ignore="MissingTranslation">After cell data</string>
<string name="pref_table_column_headers_entry_before" tools:ignore="MissingTranslation">Before cell data</string>
<string name="pref_table_column_headers_entry_off" tools:ignore="MissingTranslation">Do not read</string>
<string name="pref_table_speak_row_column_numbers_title" tools:ignore="MissingTranslation">Speak row and column numbers</string>
<string name="pref_table_speak_row_column_numbers_summary" tools:ignore="MissingTranslation">Announce row and column numbers for table cells</string>

<!--
Source: Preference for speaking time.
Description: The user can check or not check this. [CHAR LIMIT=40]
Expand Down
Loading
Loading