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