/*
* Copyright (C) 2009 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.formatter;
import android.content.res.Resources;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import com.google.android.marvin.talkback.TalkBackService;
import com.android.talkback.Utterance;
import com.android.utils.LogUtils;
import com.android.talkback.formatter.EventSpeechRule.AccessibilityEventFilter;
import com.android.talkback.formatter.EventSpeechRule.AccessibilityEventFormatter;
import org.w3c.dom.Document;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
/**
* This class is a {@link EventSpeechRule} processor responsible for loading
* from speech strategy XML files sets of {@link EventSpeechRule}s used for
* processing {@link AccessibilityEvent}s such that utterances are generated.
* <p>
* Speech strategies can be registered for handling events from a given package
* or their rules to be appended to the default speech rules which are examined
* as fall-back if no package specific ones have matched the event. The rules
* are processed in the order they are defined and in case a rule is
* successfully applied i.e. an utterance is formatted, processing stops. In
* other words, the first applicable speech rule wins.
* </p>
*/
public class EventSpeechRuleProcessor {
private static final String TAG = "EventSpeechRuleProcesso";
/**
* Indicates the result of filtering and formatting an
* {@link AccessibilityEvent} through the processor.
*/
private enum RuleProcessorResult {
/**
* Result indicating that an {@link EventSpeechRule} matched an
* {@link AccessibilityEvent} and formatted an {@link Utterance}
*/
FORMATTED,
/**
* Result indicating that no {@link EventSpeechRule}'s
* {@link AccessibilityEventFilter} matched an
* {@link AccessibilityEvent}
*/
NOT_MATCHED,
/**
* Result indicating that an {@link EventSpeechRule}'s
* {@link AccessibilityEventFilter} matched an
* {@link AccessibilityEvent}, but its
* {@link AccessibilityEventFormatter} indicated the event should be
* dropped from the processor.
*/
REJECTED
}
/**
* Constant used for storing all speech rules that either do not define a
* filter package or have custom filters.
*/
private static final String UNDEFINED_PACKAGE_NAME = "undefined_package_name";
/** Context for accessing resources. */
private final TalkBackService mContext;
/** Mapping from package name to speech rules for that package. */
private final Map<String, List<EventSpeechRule>> mPackageNameToSpeechRulesMap = new HashMap<>();
/** A lazily-constructed shared instance of a document builder. */
private DocumentBuilder mDocumentBuilder;
/**
* Creates a new instance of a speech rule processor.
*
* @param context The service context.
*/
public EventSpeechRuleProcessor(TalkBackService context) {
mContext = context;
}
/**
* Processes an {@code event} by sequentially trying to apply all
* {@link EventSpeechRule}s maintained by this processor in the order they
* are defined for the package source of the event. If no package specific
* rules exist the default speech rules are examined in the same manner. If
* a rule is successfully applied the result is used to populate an
* {@code utterance}. In other words, the first matching rule wins.
*
* @return {@code true} if the event was processed, {@code false} otherwise.
*/
public boolean processEvent(AccessibilityEvent event, Utterance utterance) {
synchronized (mPackageNameToSpeechRulesMap) {
// Try package specific speech rules first.
List<EventSpeechRule> speechRules = mPackageNameToSpeechRulesMap
.get(event.getPackageName());
if ((speechRules != null)) {
RuleProcessorResult packageResult = processEvent(speechRules, event, utterance);
switch (packageResult) {
case FORMATTED:
return true;
case REJECTED:
return false;
case NOT_MATCHED:
break;
}
}
// Package specific rule not found; try undefined package ones.
speechRules = mPackageNameToSpeechRulesMap.get(UNDEFINED_PACKAGE_NAME);
if ((speechRules != null)) {
return processEvent(speechRules, event, utterance) == RuleProcessorResult.FORMATTED;
}
}
return false;
}
/**
* Loads a speech strategy from a given <code>resourceId</code> to handle events from
* the specified <code>targetPackage</code> and use the resources from a given <code>context
* </code>. If the target package is <code>null</code> the rules of the loaded
* speech strategy are appended to the default speech rules. While for
* loading of resources is used the provided context instance, for loading
* plug-in classes (custom Filters and Formatters) the <code>publicSourceDir</code> which
* specifies the location of the APK that defines them is used to enabled
* using the TalkBack {@link ClassLoader}.
*/
public void addSpeechStrategy(int resourceId) {
final Resources res = mContext.getResources();
final String speechStrategy = res.getResourceName(resourceId);
final InputStream inputStream = res.openRawResource(resourceId);
final Document document = parseSpeechStrategy(inputStream);
final ArrayList<EventSpeechRule> speechRules = EventSpeechRule.createSpeechRules(
mContext, document);
final int added = addSpeechStrategy(speechRules);
LogUtils.log(EventSpeechRuleProcessor.class, Log.INFO, "%d speech rules appended from: %s",
added, speechStrategy);
}
/**
* Loads speech rules from a list.
*
* @return The number of rules that were loaded successfully.
*/
public int addSpeechStrategy(Iterable<EventSpeechRule> speechRules) {
int count = 0;
synchronized (mPackageNameToSpeechRulesMap) {
for (EventSpeechRule speechRule : speechRules) {
if (addSpeechRuleLocked(speechRule)) {
count++;
}
}
}
return count;
}
/**
* Adds a <code>speechRule</code>.
*/
private boolean addSpeechRuleLocked(EventSpeechRule speechRule) {
final String packageName = speechRule.getPackageName();
List<EventSpeechRule> packageSpeechRules = mPackageNameToSpeechRulesMap.get(packageName);
if (packageSpeechRules == null) {
packageSpeechRules = new LinkedList<>();
mPackageNameToSpeechRulesMap.put(packageName, packageSpeechRules);
}
return packageSpeechRules.add(speechRule);
}
/**
* Processes an {@code event} by sequentially trying to apply all
* {@code speechRules} in the order they are defined. If a rule is
* successfully applied the result is used to populate an {@code Utterance}.
*
* @return a {@link RuleProcessorResult} corresponding to how the event was
* processed.
*/
private RuleProcessorResult processEvent(
List<EventSpeechRule> speechRules, AccessibilityEvent event, Utterance utterance) {
for (EventSpeechRule speechRule : speechRules) {
// We should never crash because of a bug in speech rules.
try {
if (speechRule.applyFilter(event)) {
if (speechRule.applyFormatter(event, utterance)) {
if (LogUtils.LOG_LEVEL <= Log.VERBOSE) {
Log.v(TAG, String.format("Processed event using rule: \n%s",
speechRule));
}
return RuleProcessorResult.FORMATTED;
} else {
if (LogUtils.LOG_LEVEL <= Log.VERBOSE) {
AccessibilityEventFilter filter = speechRule.getFilter();
if (filter != null) {
Log.v(TAG, String.format("The \"%s\" filter accepted the event, but"
+ " the \"%s\" formatter indicated the event should"
+ " be dropped.",
filter.getClass().getSimpleName(),
speechRule.getFormatter().getClass().getSimpleName()));
}
}
return RuleProcessorResult.REJECTED;
}
}
} catch (Exception e) {
LogUtils.log(EventSpeechRuleProcessor.class, Log.ERROR,
"Error while processing rule:\n%s", speechRule);
e.printStackTrace();
}
}
return RuleProcessorResult.NOT_MATCHED;
}
/**
* Parses a speech strategy XML file specified by <code>resourceId</code> and returns
* a <code>document</code>. If an error occurs during the parsing, it is logged and
* <code>null</code> is returned.
*
* @param inputStream An {@link InputStream} to the speech strategy XML
* file.
* @return The parsed {@link Document} or <code>null</code> if an error
* occurred.
*/
private Document parseSpeechStrategy(InputStream inputStream) {
try {
final DocumentBuilder builder = getDocumentBuilder();
return builder.parse(inputStream);
} catch (Exception e) {
LogUtils.log(EventSpeechRuleProcessor.class, Log.ERROR,
"Could not open speechstrategy xml file\n%s", e.toString());
}
return null;
}
/**
* @return A lazily-constructed shared instance of a document builder.
* @throws ParserConfigurationException
*/
private DocumentBuilder getDocumentBuilder() throws ParserConfigurationException {
if (mDocumentBuilder == null) {
mDocumentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
}
return mDocumentBuilder;
}
}