/*
* Copyright (C) 2015 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;
import android.test.suitebuilder.annotation.SmallTest;
import com.android.talkback.formatter.EventSpeechRule;
import com.android.talkback.formatter.EventSpeechRule.AccessibilityEventFormatter;
import com.android.talkback.formatter.EventSpeechRuleProcessor;
import android.text.TextUtils;
import android.view.accessibility.AccessibilityEvent;
import com.google.android.marvin.talkback.TalkBackService;
import com.googlecode.eyesfree.testing.TalkBackInstrumentationTestCase;
import com.android.utils.StringBuilderUtils;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
/**
* This class is a test case for the {@link EventSpeechRule} class.
*/
public class SpeechRuleTest extends TalkBackInstrumentationTestCase {
private TalkBackService mTalkBack;
private static final String TEMPLATE_SPEECH_STRATEGY =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
" <ss:speechstrategy " +
" xmlns:ss=\"http://www.google.android.marvin.talkback.com/speechstrategy\" " +
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " +
" xsi:schemaLocation=\"http://www.google.android.marvin.talkback.com/speechstrategy speechstrategy.xsd \">" +
"%1s" +
"</ss:speechstrategy>";
@Override
protected void setUp() throws Exception {
super.setUp();
mTalkBack = getService();
}
/**
* Test if the {@link EventSpeechRule} returns an empty list if it is passed
* a <code>null</code> document.
*/
@SmallTest
public void testCreateSpeechRules_fromNullDocument() throws Exception {
final List<EventSpeechRule> speechRules = EventSpeechRule.createSpeechRules(
mTalkBack, null);
assertNotNull("Must always return an instance.", speechRules);
assertTrue("The list must be empty.", speechRules.isEmpty());
}
/**
* Test if an event is filtered correctly when all its properties are used.
*/
@SmallTest
public void testCreateSpeechRules_filteringByTextProperty() throws Exception {
final String strategy =
"<ss:rule>" +
" <ss:filter>" +
" <ss:text>first blank second</ss:text>" +
" </ss:filter>" +
" <ss:formatter>" +
" <ss:template>template</ss:template>" +
" </ss:formatter>" +
"</ss:rule>";
// Create an event that matches our only rule.
final AccessibilityEvent event = AccessibilityEvent.obtain();
event.getText().add("first blank second");
final EventSpeechRuleProcessor processor = createProcessorWithStrategy(strategy, 1);
final Utterance utterance = new Utterance();
final boolean processed = processor.processEvent(event, utterance);
assertTrue("The event must match the filter", processed);
assertFalse("An utterance must be produced",
TextUtils.isEmpty(StringBuilderUtils.getAggregateText(utterance.getSpoken())));
}
/**
* Test if an event is filtered correctly when all its properties are used.
*/
@SmallTest
public void testCreateSpeechRules_filteringByAllEventProperties() throws Exception {
final String strategy =
"<ss:rule>" +
" <ss:filter>" +
" <ss:addedCount>1</ss:addedCount>" +
" <ss:beforeText>beforeText</ss:beforeText>" +
" <ss:checked>true</ss:checked>" +
" <ss:className>foo.bar.baz.Test</ss:className>" +
" <ss:contentDescription>contentDescription</ss:contentDescription>" +
" <ss:currentItemIndex>2</ss:currentItemIndex>" +
" <ss:enabled>true</ss:enabled>" +
" <ss:eventType>TYPE_NOTIFICATION_STATE_CHANGED</ss:eventType>" +
" <ss:fromIndex>1</ss:fromIndex>" +
" <ss:fullScreen>true</ss:fullScreen>" +
" <ss:itemCount>10</ss:itemCount>" +
" <ss:packageName>foo.bar.baz</ss:packageName>" +
" <ss:password>true</ss:password>" +
" <ss:removedCount>2</ss:removedCount>" +
" <ss:contentDescriptionOrText>contentDescription</ss:contentDescriptionOrText>" +
" </ss:filter>" +
" <ss:formatter>" +
" <ss:template>text template</ss:template>" +
" </ss:formatter>" +
"</ss:rule>";
// Create an event that matches our only rule.
final AccessibilityEvent event = AccessibilityEvent.obtain();
event.setAddedCount(1);
event.setBeforeText("beforeText");
event.setChecked(true);
event.setClassName("foo.bar.baz.Test");
event.setContentDescription("contentDescription");
event.setCurrentItemIndex(2);
event.setEnabled(true);
event.setEventType(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
event.setFromIndex(1);
event.setFullScreen(true);
event.setItemCount(10);
event.setPackageName("foo.bar.baz");
event.setPassword(true);
event.setRemovedCount(2);
// The event text isn't checked by the rule since it's overridden by the
// content description.
event.getText().add("first blank second");
final EventSpeechRuleProcessor processor = createProcessorWithStrategy(strategy, 1);
final Utterance utterance = new Utterance();
final boolean processed = processor.processEvent(event, utterance);
assertTrue("The event must match the filter", processed);
assertFalse("An utterance must be produced",
TextUtils.isEmpty(StringBuilderUtils.getAggregateText(utterance.getSpoken())));
assertTrue(
"The utterance must contain the template text of the formatter", StringBuilderUtils
.getAggregateText(utterance.getSpoken()).toString()
.contains("text template"));
}
/**
* Test if the utterance for speaking an event is properly constructed if
* the speech rule defines a template which is to be populated by event
* property values.
*/
@SmallTest
public void testCreateSpeechRules_useRuleWithPropertyValuesFormatter() throws Exception {
final String strategy =
"<ss:rule>" +
" <ss:formatter>" +
" <ss:template>@string/template_long_clicked</ss:template>" +
" <ss:property>packageName</ss:property>" +
" </ss:formatter>" +
"</ss:rule>";
// Create an event with just a package name.
final AccessibilityEvent event = AccessibilityEvent.obtain();
event.setPackageName("foo.bar.baz");
final EventSpeechRuleProcessor processor = createProcessorWithStrategy(strategy, 1);
final Utterance utterance = new Utterance();
final boolean processed = processor.processEvent(event, utterance);
assertTrue("The event must match the filter", processed);
assertFalse("An utterance must be produced",
TextUtils.isEmpty(StringBuilderUtils.getAggregateText(utterance.getSpoken())));
}
/**
* Test if the utterance for speaking an event is properly constructed if
* the speech rule defines a template which is a plural resource and
* quantity.
*/
@SmallTest
public void testCreateSpeechRules_useRuleWithQuantityFormatter() throws Exception {
final String strategy =
"<ss:rule>" +
" <ss:formatter>" +
" <ss:template>@plurals/template_containers</ss:template>" +
" <ss:quantity>itemCount</ss:quantity>" +
" <ss:property>contentDescriptionOrText</ss:property>" +
" <ss:property>itemCount</ss:property>" +
" </ss:formatter>" +
"</ss:rule>";
// Create an event to match the "one" quantity of @plurals/template_containers
final AccessibilityEvent oneEvent = AccessibilityEvent.obtain();
oneEvent.setPackageName("foo.bar.baz");
oneEvent.setContentDescription("List");
oneEvent.setItemCount(1);
final EventSpeechRuleProcessor processor = createProcessorWithStrategy(strategy, 1);
Utterance utterance = new Utterance();
boolean processed = processor.processEvent(oneEvent, utterance);
assertTrue("The singleEvent must match the filter", processed);
assertFalse("An utterance must be produced",
TextUtils.isEmpty(StringBuilderUtils.getAggregateText(utterance.getSpoken())));
assertFalse(
"The utterance produced by singleEvent must use the \"one\" plural option from the template resource",
StringBuilderUtils.getAggregateText(utterance.getSpoken()).toString().contains(
"items"));
// Create an event to match the "other" quantity of @plurals/template_containers
final AccessibilityEvent otherEvent = AccessibilityEvent.obtain();
otherEvent.setPackageName("foo.bar.baz");
otherEvent.setContentDescription("List");
otherEvent.setItemCount(6);
utterance = new Utterance();
processed = processor.processEvent(otherEvent, utterance);
assertTrue("The singleEvent must match the filter", processed);
assertFalse("An utterance must be produced",
TextUtils.isEmpty(StringBuilderUtils.getAggregateText(utterance.getSpoken())));
assertTrue(
"The utterance produced by singleEvent must use the \"one\" plural option from the template resource",
StringBuilderUtils.getAggregateText(utterance.getSpoken()).toString().contains(
"items"));
}
/**
* Test if an event is dropped on the floor if no formatter is specified.
*/
@SmallTest
public void testCreateSpeechRules_dropEventIfNoFormatter() throws Exception {
final String strategy =
"<ss:rule>" +
" <ss:filter>" +
" <ss:eventType>TYPE_VIEW_CLICKED</ss:eventType>" +
" </ss:filter>" +
"</ss:rule>";
// Create an empty CLICKED event.
final AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_VIEW_CLICKED);
final EventSpeechRuleProcessor processor = createProcessorWithStrategy(strategy, 1);
final Utterance utterance = new Utterance();
final boolean processed = processor.processEvent(event, utterance);
assertTrue("The event must match the filter", processed);
assertTrue("No utterance should be produced",
TextUtils.isEmpty(StringBuilderUtils.getAggregateText(utterance.getSpoken())));
}
/**
* Test if the utterance for speaking an event is properly constructed if
* the a speech rule defines a template which is to be populated by event
* property values.
*/
@SmallTest
public void testCreateSpeechRules_multipleRuleParsing() throws Exception {
final String strategy =
"<ss:rule>" +
" <ss:formatter>" +
" <ss:property>packageName</ss:property>" +
" </ss:formatter>" +
"</ss:rule>" +
"<ss:rule>" +
" <ss:formatter>" +
" <ss:property>packageName</ss:property>" +
" </ss:formatter>" +
"</ss:rule>";
loadSpeechRulesAssertingCorrectness(strategy, 2);
}
/**
* Test if the utterance for speaking an event is properly constructed if a
* {@link AccessibilityEventFormatter} is being used.
*/
@SmallTest
public void testCreateSpeechRules_customFormatter() throws Exception {
final String speechStrategyContent =
"<ss:rule>" +
" <ss:formatter>" +
" <ss:custom>com.android.talkback.formatter.TextFormatters$ChangedTextFormatter</ss:custom>" +
" </ss:formatter>" +
"</ss:rule>";
loadSpeechRulesAssertingCorrectness(speechStrategyContent, 1);
}
/**
* Test that the meta-data of a speech rule is properly parsed and passed to
* a formatted utterance.
*/
@SmallTest
public void testCreateSpeechRules_metadata() throws Exception {
final String strategy =
"<ss:rule>" +
" <ss:metadata>" +
" <ss:queuing>UNINTERRUPTIBLE</ss:queuing>" +
" </ss:metadata>" +
"</ss:rule>";
// Create an empty event.
final AccessibilityEvent event = AccessibilityEvent.obtain();
final EventSpeechRuleProcessor processor = createProcessorWithStrategy(strategy, 1);
final Utterance utterance = new Utterance();
final boolean processed = processor.processEvent(event, utterance);
assertTrue("The event must match the filter", processed);
assertEquals("The meta-data must have its queuing poperty set", 2,
utterance.getMetadata().get(Utterance.KEY_METADATA_QUEUING));
}
/**
* Test that a filter with an empty property match tag matches only events where that property
* is the empty string or null.
* (This test is mainly here to verify the behavior of several existing rules that have
* empty property tags.
* New rules should not use an empty value for filter property fields because it is not
* immediately apparent what that does.
* New rules should explicitly use the {@code require="empty"} attribute instead.)
*/
@SmallTest
public void testCreateSpeechRules_filter_emptyProperty() throws Exception {
final String strategy =
"<ss:rule>" +
" <ss:filter>" +
" <ss:contentDescription></ss:contentDescription>" +
" <ss:text></ss:text>" +
" </ss:filter>" +
"</ss:rule>";
// Create an event with empty ("") contentDescription.
final AccessibilityEvent event = AccessibilityEvent.obtain();
event.setContentDescription("");
event.getText().add("");
final EventSpeechRuleProcessor processor = createProcessorWithStrategy(strategy, 1);
final Utterance utterance = new Utterance();
final boolean emptyProcessed = processor.processEvent(event, utterance);
assertTrue("The empty-contentDescription event must match the filter", emptyProcessed);
event.setContentDescription(null);
final boolean nullProcessed = processor.processEvent(event, utterance);
assertTrue("The null-contentDescription event must match the filter", nullProcessed);
event.setContentDescription("Awesome");
final boolean nonEmptyProcessed = processor.processEvent(event, utterance);
assertFalse("The non-empty event must not match the filter", nonEmptyProcessed);
}
/**
* Similar to {@link #testCreateSpeechRules_filter_emptyProperty} but checks for rules that
* have explicitly marked the {@code require="empty"} attribute.
*/
@SmallTest
public void testCreateSpeechRules_filter_requireEmptyProperty() throws Exception {
final String strategy =
"<ss:rule>" +
" <ss:filter>" +
" <ss:contentDescriptionOrText require='empty' />" +
" </ss:filter>" +
"</ss:rule>";
// Create an event with empty ("") contentDescription.
final AccessibilityEvent event = AccessibilityEvent.obtain();
event.setContentDescription("");
final EventSpeechRuleProcessor processor = createProcessorWithStrategy(strategy, 1);
final Utterance utterance = new Utterance();
final boolean emptyProcessed = processor.processEvent(event, utterance);
assertTrue("The empty-contentDescription event must match the filter", emptyProcessed);
event.setContentDescription(null);
final boolean nullProcessed = processor.processEvent(event, utterance);
assertTrue("The null-contentDescription event must match the filter", nullProcessed);
event.setContentDescription("Awesome");
final boolean nonEmptyProcessed = processor.processEvent(event, utterance);
assertFalse("The non-empty event must not match the filter", nonEmptyProcessed);
}
/**
* The opposite of {@link #testCreateSpeechRules_filter_requireEmptyProperty}; checks for rules
* that have explicitly marked the {@code require="nonEmpty"} attribute.
*/
@SmallTest
public void testCreateSpeechRules_filter_requireNonEmptyProperty() throws Exception {
final String strategy =
"<ss:rule>" +
" <ss:filter>" +
" <ss:contentDescriptionOrText require='nonEmpty' />" +
" </ss:filter>" +
"</ss:rule>";
// Create an event with empty ("") contentDescription.
final AccessibilityEvent event = AccessibilityEvent.obtain();
event.setContentDescription("");
final EventSpeechRuleProcessor processor = createProcessorWithStrategy(strategy, 1);
final Utterance utterance = new Utterance();
final boolean emptyProcessed = processor.processEvent(event, utterance);
assertFalse("The empty-contentDescription event must not match the filter", emptyProcessed);
event.setContentDescription(null);
final boolean nullProcessed = processor.processEvent(event, utterance);
assertFalse("The null-contentDescription event must not match the filter", nullProcessed);
event.setContentDescription("Awesome");
final boolean nonEmptyProcessed = processor.processEvent(event, utterance);
assertTrue("The non-empty (cD) event must match the filter", nonEmptyProcessed);
event.setContentDescription(null);
event.getText().add("Awesome");
final boolean nonEmptyTextProcessed = processor.processEvent(event, utterance);
assertTrue("The non-empty (text) event must match the filter", nonEmptyTextProcessed);
}
private EventSpeechRuleProcessor createProcessorWithStrategy(String strategy, int ruleCount)
throws Exception {
final EventSpeechRuleProcessor processor = new EventSpeechRuleProcessor(mTalkBack);
final List<EventSpeechRule> speechRules = loadSpeechRulesAssertingCorrectness(
strategy, ruleCount);
processor.addSpeechStrategy(speechRules);
return processor;
}
/**
* Loads the {@link EventSpeechRule}s from a document obtained by parsing a
* XML string generated by populating the {@link #TEMPLATE_SPEECH_STRATEGY}
* with <code>speechStrategyContent</code>. The method asserts the document is parsed and the
* parsed speech rules list has <code>expectedSize</code>.
*
* @return The parsed speech rules.
*/
private List<EventSpeechRule> loadSpeechRulesAssertingCorrectness(
String strategy, int expectedSize) throws Exception {
final String xmlStrategy = String.format(TEMPLATE_SPEECH_STRATEGY, strategy);
final DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
final Document document = builder.parse(new InputSource(new StringReader(xmlStrategy)));
assertNotNull("Test case setup requires properly parsed document.", document);
final ArrayList<EventSpeechRule> speechRules = EventSpeechRule.createSpeechRules(
mTalkBack, document);
assertEquals("There must be " + expectedSize + " rules", expectedSize, speechRules.size());
return speechRules;
}
}