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