PageRenderTime 42ms CodeModel.GetById 1ms app.highlight 35ms RepoModel.GetById 1ms app.codeStats 1ms

/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
 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}