/TalkBack/src/com/google/android/marvin/talkback/SpeechRuleProcessor.java

http://eyes-free.googlecode.com/ · Java · 299 lines · 147 code · 26 blank · 126 comment · 17 complexity · 8f73e08bb588fc14c37fec9c05b07a5d MD5 · raw file

  1. /*
  2. * Copyright (C) 2009 The Android Open Source Project
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package com.google.android.marvin.talkback;
  17. import android.content.Context;
  18. import android.os.Bundle;
  19. import android.util.Log;
  20. import android.view.accessibility.AccessibilityEvent;
  21. import org.w3c.dom.Document;
  22. import java.io.File;
  23. import java.io.FileInputStream;
  24. import java.io.FileNotFoundException;
  25. import java.io.InputStream;
  26. import java.util.ArrayList;
  27. import java.util.HashMap;
  28. import java.util.Iterator;
  29. import javax.xml.parsers.DocumentBuilder;
  30. import javax.xml.parsers.DocumentBuilderFactory;
  31. /**
  32. * This class is a {@link SpeechRule} processor responsible for loading from
  33. * speech strategy XML files sets of {@link SpeechRule}s used for processing
  34. * {@link AccessibilityEvent}s such that utterances are generated. Speech
  35. * strategies can be registered for handling events from a given package or
  36. * their rules to be appended to the default speech rules which are examined as
  37. * fall-back if no package specific ones have matched the event. The rules are
  38. * processed in the order they are defined and in case a rule is successfully
  39. * applied i.e. an utterance is formatted, processing stops. In other words, the
  40. * first applicable speech rule wins.
  41. *
  42. * @author svetoslavganov@google.com (Svetoslav Ganov)
  43. */
  44. class SpeechRuleProcessor {
  45. /**
  46. * Constant used for storing all speech rules that either do not
  47. * define a filter package or have custom filters.
  48. */
  49. private static final String UNDEFINED_PACKAGE_NAME = "undefined_package_name";
  50. /**
  51. * Mutex lock for accessing the speech rules.
  52. */
  53. private final Object mLock = new Object();
  54. /**
  55. * Context for accessing resources.
  56. */
  57. private final Context mContext;
  58. /**
  59. * Mapping from package name to speech rules for that package.
  60. */
  61. private HashMap<String, ArrayList<SpeechRule>> mPackageNameToSpeechRulesMap = new HashMap<String, ArrayList<SpeechRule>>();
  62. /**
  63. * Creates a new instance.
  64. */
  65. public SpeechRuleProcessor(Context context) {
  66. mContext = context;
  67. }
  68. /**
  69. * Loads a speech strategy from a given <code>resourceId</code> to handle
  70. * events from all packages and use the resources of the TalkBack context.
  71. */
  72. void addSpeechStrategy(int resourceId) {
  73. addSpeechStrategy(mContext, null, null, resourceId);
  74. }
  75. /**
  76. * Removes the speech rules defined by a speech strategy with the
  77. * given <code>resourceId</code>
  78. */
  79. void removeSpeechStrategy(int resourceId) {
  80. String speechStrategy = mContext.getResources().getResourceName(resourceId);
  81. removeSpeechStrategy(speechStrategy);
  82. }
  83. /**
  84. * Loads a speech strategy from a given <code>file</code>.
  85. */
  86. void addSpeechStrategy(File file) {
  87. String speechStrategy = file.toURI().toString();
  88. try {
  89. InputStream inputStream = new FileInputStream(file);
  90. addSpeechStrategy(mContext, speechStrategy, null, null, inputStream);
  91. } catch (FileNotFoundException fnfe) {
  92. LogUtils.log(SpeechRuleProcessor.class, Log.ERROR, "Error loading speech strategy: "
  93. + "%s\n%s", speechStrategy, fnfe.toString());
  94. }
  95. }
  96. /**
  97. * Removes the speech rules defined by a speech strategy in the
  98. * given <code>file</code>
  99. */
  100. void removeSpeechStrategy(File file) {
  101. String speechStrategy = file.toURI().toString();
  102. removeSpeechStrategy(speechStrategy);
  103. }
  104. /**
  105. * Loads a speech strategy from a given <code>resourceId</code> to handle
  106. * events from the specified <code>targetPackage</code> and use the resources
  107. * from a given <code>context</code>. If the target package is
  108. * <code>null</code> the rules of the loaded speech strategy are appended to
  109. * the default speech rules. While for loading of resources is used the provided
  110. * context instance, for loading plug-in classes (custom Filters and Formatters)
  111. * the <code>publicSourceDir</code> which specifies the location of the APK that
  112. * defines them is used to enabled using the TalkBack {@link ClassLoader}.
  113. */
  114. void addSpeechStrategy(Context context, String publicSourceDir, String targetPackage,
  115. int resourceId) {
  116. String speechStrategy = context.getResources().getResourceName(resourceId);
  117. InputStream inputStream = context.getResources().openRawResource(resourceId);
  118. addSpeechStrategy(context, speechStrategy, targetPackage, publicSourceDir, inputStream);
  119. }
  120. /**
  121. * Loads a <code>speechStrategy</code> from a given <code>inputStream</code> to handle
  122. * events from the specified <code>targetPackage</code> and use the resources
  123. * from a given <code>context</code>. If the target package is
  124. * <code>null</code> the rules of the loaded speech strategy are appended to
  125. * the default speech rules. While for loading of resources is used the provided
  126. * context instance, for loading plug-in classes (custom Filters and Formatters)
  127. * from the <code>publicSourceDir</code> (specifies the location of the APK that
  128. * defines them) is used the TalkBack {@link ClassLoader}.
  129. */
  130. private void addSpeechStrategy(Context context, String speechStrategy, String targetPackage,
  131. String publicSourceDir, InputStream inputStream) {
  132. Document document = parseSpeechStrategy(context, inputStream);
  133. ArrayList<SpeechRule> speechRules = SpeechRule.createSpeechRules(context, speechStrategy,
  134. targetPackage, publicSourceDir, document);
  135. synchronized (mLock) {
  136. for (int i = 0, count = speechRules.size(); i < count; i++) {
  137. SpeechRule speechRule = speechRules.get(i);
  138. addSpeechRuleLocked(speechRule);
  139. }
  140. }
  141. LogUtils.log(SpeechRuleProcessor.class, Log.INFO, "%d speech rules appended from: %s",
  142. speechRules.size(), speechStrategy);
  143. }
  144. /**
  145. * Removes the speech rules defined by a given <code>speechStrategy</code>
  146. */
  147. private void removeSpeechStrategy(String speechStrategy) {
  148. int removedCount = 0;
  149. for (String key : mPackageNameToSpeechRulesMap.keySet()) {
  150. ArrayList<SpeechRule> speechRules = mPackageNameToSpeechRulesMap.get(key);
  151. Iterator<SpeechRule> speechRulesIterator = speechRules.iterator();
  152. while (speechRulesIterator.hasNext()) {
  153. SpeechRule speechRule = speechRulesIterator.next();
  154. if (speechStrategy.equals(speechRule.getSpeechStrategy())) {
  155. speechRulesIterator.remove();
  156. removedCount++;
  157. }
  158. }
  159. }
  160. LogUtils.log(SpeechRuleProcessor.class, Log.DEBUG, "%d speech rules removed from: %s",
  161. removedCount, speechStrategy);
  162. }
  163. /**
  164. * Adds a <code>speechRule</code>.
  165. */
  166. private boolean addSpeechRuleLocked(SpeechRule speechRule) {
  167. String packageName = speechRule.getPackageName();
  168. ArrayList<SpeechRule> packageSpeechRules = mPackageNameToSpeechRulesMap.get(packageName);
  169. if (packageSpeechRules == null) {
  170. packageSpeechRules = new ArrayList<SpeechRule>();
  171. mPackageNameToSpeechRulesMap.put(packageName, packageSpeechRules);
  172. }
  173. return packageSpeechRules.add(speechRule);
  174. }
  175. /**
  176. * Removes the speech rules for a given <code>packageName</code>.
  177. *
  178. * @return True if speech rules were removed.
  179. */
  180. boolean removeSpeechRulesForPackage(String packageName) {
  181. synchronized (mLock) {
  182. ArrayList<SpeechRule> speechRules = mPackageNameToSpeechRulesMap.remove(packageName);
  183. if (speechRules != null) {
  184. LogUtils.log(SpeechRuleProcessor.class, Log.INFO, "%d speech rules removed",
  185. speechRules.size());
  186. }
  187. return (speechRules != null);
  188. }
  189. }
  190. /**
  191. * Processes an <code>event</code> by sequentially trying to apply all
  192. * {@link SpeechRule}s in the order they are defined for the package source
  193. * of the event. If no package specific rules exist the default speech rules
  194. * are examined in the same manner. If a rule is successfully applied the
  195. * result is used to populate an <code>utterance</code>. In other words, the
  196. * first matching rule wins. Optionally <code>filterArguments</code> and
  197. * <code>formatterArguments</code> can be provided.
  198. *
  199. * @return True if the event was processed false otherwise.
  200. */
  201. boolean processEvent(AccessibilityEvent event, Utterance utterance, Bundle filterArguments,
  202. Bundle formatterArguments) {
  203. synchronized (mLock) {
  204. // Try package specific speech rules first.
  205. ArrayList<SpeechRule> speechRules = mPackageNameToSpeechRulesMap
  206. .get(event.getPackageName());
  207. if (speechRules != null
  208. && processEvent(speechRules, event, utterance, filterArguments,
  209. formatterArguments)) {
  210. return true;
  211. }
  212. // Package specific rule not found; try undefined package ones.
  213. speechRules = mPackageNameToSpeechRulesMap.get(UNDEFINED_PACKAGE_NAME);
  214. if (speechRules != null
  215. && processEvent(speechRules, event, utterance, filterArguments,
  216. formatterArguments)) {
  217. return true;
  218. }
  219. }
  220. return false;
  221. }
  222. /**
  223. * Processes an <code>event</code> by sequentially trying to apply all
  224. * <code>speechRules</code> in the order they are defined for the package
  225. * source of the event. If no package specific rules exist the default
  226. * speech rules are examined in the same manner. If a rule is successfully
  227. * applied the result is used to populate an <code>utterance</code>. In
  228. * other words, the first matching rule wins. Optionally
  229. * <code>filterArguments</code> and <code>formatterArguments</code> can be
  230. * provided.
  231. *
  232. * @return {@code true} if the event was processed, {@code false} otherwise.
  233. */
  234. private boolean processEvent(ArrayList<SpeechRule> speechRules, AccessibilityEvent event,
  235. Utterance utterance, Bundle filterArguments, Bundle formatterArguments) {
  236. for (int i = 0, count = speechRules.size(); i < count; i++) {
  237. // We should never crash because of a bug in speech rules.
  238. SpeechRule speechRule = speechRules.get(i);
  239. try {
  240. if (speechRule.apply(event, utterance, filterArguments, formatterArguments)) {
  241. LogUtils.log(SpeechRuleProcessor.class, Log.VERBOSE, "Processed event using "
  242. + "rule:\n%s", speechRule.asXmlString());
  243. return true;
  244. }
  245. } catch (Throwable t) {
  246. LogUtils.log(SpeechRuleProcessor.class, Log.ERROR, "Error while processing "
  247. + "rule:\n%s", speechRule.asXmlString());
  248. t.printStackTrace();
  249. }
  250. }
  251. return false;
  252. }
  253. /**
  254. * Parses a speech strategy XML file specified by <code>resourceId</code>
  255. * and returns a <code>document</code>. If an error occurs during the
  256. * parsing, it is logged and <code>null</code> is returned.
  257. *
  258. * @param context A {@link Context} instance.
  259. * @param inputStream An {@link InputStream} to the speech strategy XML file.
  260. * @return The parsed {@link Document} or <code>null</code> if an error
  261. * occurred.
  262. */
  263. private Document parseSpeechStrategy(Context context, InputStream inputStream) {
  264. try {
  265. DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
  266. return builder.parse(inputStream);
  267. } catch (Exception e) {
  268. LogUtils.log(SpeechRuleProcessor.class, Log.ERROR, "Could not open speechstrategy "
  269. + "xml file\n%s", e.toString());
  270. }
  271. return null;
  272. }
  273. }