PageRenderTime 114ms CodeModel.GetById 16ms app.highlight 86ms RepoModel.GetById 1ms app.codeStats 1ms

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

http://eyes-free.googlecode.com/
Java | 1374 lines | 865 code | 133 blank | 376 comment | 280 complexity | a4ccecb2c1d79033f022a8f55d56691a MD5 | raw file

Large files files are truncated, but you can click here to view the full 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.Build;
  21import android.os.Bundle;
  22import android.util.Log;
  23import android.util.Pair;
  24import android.view.accessibility.AccessibilityEvent;
  25
  26import dalvik.system.DexFile;
  27
  28import org.w3c.dom.Document;
  29import org.w3c.dom.Node;
  30import org.w3c.dom.NodeList;
  31
  32import java.io.File;
  33import java.io.StringWriter;
  34import java.util.ArrayList;
  35import java.util.HashMap;
  36import java.util.LinkedList;
  37import java.util.List;
  38import java.util.MissingFormatArgumentException;
  39import java.util.regex.Pattern;
  40
  41import javax.xml.transform.OutputKeys;
  42import javax.xml.transform.Transformer;
  43import javax.xml.transform.TransformerException;
  44import javax.xml.transform.TransformerFactory;
  45import javax.xml.transform.dom.DOMSource;
  46import javax.xml.transform.stream.StreamResult;
  47
  48/**
  49 * This class represents a speech rule for an {@link AccessibilityEvent}. The
  50 * rule has a filter which determines if the rule applies to a given event. If
  51 * the rule applies to an event the formatter for that rule is used to generate
  52 * a formatted utterance.
  53 * 
  54 * @author svetoslavganov@google.com (Svetoslav Ganov)
  55 */
  56class SpeechRule {
  57
  58    // string constants
  59    private static final String SPACE = " ";
  60    private static final String COLON = ":";
  61
  62    // node names
  63    private static final String NODE_NAME_METADATA = "metadata";
  64    private static final String NODE_NAME_FILTER = "filter";
  65    private static final String NODE_NAME_FORMATTER = "formatter";
  66    private static final String NODE_NAME_CUSTOM = "custom";
  67
  68    // properties
  69    private static final String PROPERTY_EVENT_TYPE = "eventType";
  70    private static final String PROPERTY_PACKAGE_NAME = "packageName";
  71    private static final String PROPERTY_CLASS_NAME = "className";
  72    private static final String PROPERTY_TEXT = "text";
  73    private static final String PROPERTY_BEFORE_TEXT = "beforeText";
  74    private static final String PROPERTY_CONTENT_DESCRIPTION = "contentDescription";
  75    private static final String PROPERTY_EVENT_TIME = "eventTime";
  76    private static final String PROPERTY_ITEM_COUNT = "itemCount";
  77    private static final String PROPERTY_CURRENT_ITEM_INDEX = "currentItemIndex";
  78    private static final String PROPERTY_FROM_INDEX = "fromIndex";
  79    private static final String PROPERTY_TO_INDEX = "toIndex";
  80    private static final String PROPERTY_SCROLLABLE = "scrollable";
  81    private static final String PROPERTY_SCROLL_X = "scrollX";
  82    private static final String PROPERTY_SCROLL_Y = "scrollY";
  83    private static final String PROPERTY_RECORD_COUNT = "recordCount";
  84    private static final String PROPERTY_CHECKED = "checked";
  85    private static final String PROPERTY_ENABLED = "enabled";
  86    private static final String PROPERTY_FULL_SCREEN = "fullScreen";
  87    private static final String PROPERTY_PASSWORD = "password";
  88    private static final String PROPERTY_ADDED_COUNT = "addedCount";
  89    private static final String PROPERTY_REMOVED_COUNT = "removedCount";
  90    private static final String PROPERTY_QUEUING = "queuing";
  91    private static final String PROPERTY_EARCON = "earcon";
  92    private static final String PROPERTY_VIBRATION = "vibration";
  93    private static final String PROPERTY_CUSTOM_EARCON = "customEarcon";
  94    private static final String PROPERTY_CUSTOM_VIBRATION = "customVibration";
  95    private static final String PROPERTY_VERSION_CODE = "versionCode";
  96    private static final String PROPERTY_VERSION_NAME = "versionName";
  97    private static final String PROPERTY_PLATFORM_RELEASE = "platformRelease";
  98    private static final String PROPERTY_PLATFORM_SDK = "platformSdk";
  99
 100    /**
 101     * Constant used for storing all speech rules that either do not define a
 102     * filter package or have custom filters.
 103     */
 104    private static final String UNDEFINED_PACKAGE_NAME = "undefined_package_name";
 105
 106    /** Reusable builder to avoid object creation. */
 107    private static final StringBuilder sTempBuilder = new StringBuilder();
 108
 109    /** Mapping from event type name to its type. */
 110    private static final HashMap<String, Integer> sEventTypeNameToValueMap =
 111            new HashMap<String, Integer>();
 112    static {
 113        sEventTypeNameToValueMap.put("TYPE_VIEW_CLICKED", AccessibilityEvent.TYPE_VIEW_CLICKED);
 114        sEventTypeNameToValueMap.put("TYPE_VIEW_LONG_CLICKED",
 115                AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
 116        sEventTypeNameToValueMap.put("TYPE_VIEW_SELECTED", AccessibilityEvent.TYPE_VIEW_SELECTED);
 117        sEventTypeNameToValueMap.put("TYPE_VIEW_FOCUSED", AccessibilityEvent.TYPE_VIEW_FOCUSED);
 118        sEventTypeNameToValueMap.put("TYPE_VIEW_TEXT_CHANGED",
 119                AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
 120        sEventTypeNameToValueMap.put("TYPE_WINDOW_STATE_CHANGED",
 121                AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
 122        sEventTypeNameToValueMap.put("TYPE_NOTIFICATION_STATE_CHANGED",
 123                AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
 124        sEventTypeNameToValueMap.put("TYPE_VIEW_HOVER_ENTER",
 125                AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
 126        sEventTypeNameToValueMap.put("TYPE_VIEW_HOVER_EXIT",
 127                AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
 128        sEventTypeNameToValueMap.put("TYPE_TOUCH_EXPLORATION_GESTURE_START",
 129                AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START);
 130        sEventTypeNameToValueMap.put("TYPE_TOUCH_EXPLORATION_GESTURE_END",
 131                AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END);
 132        sEventTypeNameToValueMap.put("TYPE_WINDOW_CONTENT_CHANGED",
 133                AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
 134        sEventTypeNameToValueMap.put("TYPE_VIEW_SCROLLED", AccessibilityEvent.TYPE_VIEW_SCROLLED);
 135        sEventTypeNameToValueMap.put("TYPE_VIEW_TEXT_SELECTION_CHANGED",
 136                AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
 137    }
 138
 139    /**
 140     * Mapping from queue mode names to queue modes.
 141     */
 142    private static final HashMap<String, Integer> sQueueModeNameToQueueModeMap =
 143            new HashMap<String, Integer>();
 144    static {
 145        sQueueModeNameToQueueModeMap.put("INTERRUPT", 0);
 146        sQueueModeNameToQueueModeMap.put("QUEUE", 1);
 147        sQueueModeNameToQueueModeMap.put("UNINTERRUPTIBLE", 2);
 148    }
 149
 150    /**
 151     * Meta-data of how the utterance should be spoken. It is a key value
 152     * mapping to enable extending the meta-data specification.
 153     */
 154    private final Bundle mMetadata = new Bundle();
 155
 156    /**
 157     * List of earcons resource identifiers that should be played with an
 158     * utterance.
 159     */
 160    private final List<Integer> mEarcons = new LinkedList<Integer>();
 161
 162    /**
 163     * List of vibration pattern resource identifiers that should be played with
 164     * an utterance.
 165     */
 166    private final List<Integer> mVibrationPatterns = new LinkedList<Integer>();
 167
 168    /**
 169     * List of resource identifiers for preference keys that correspond to
 170     * custom earcons that should be played with an utterance.
 171     */
 172    private final List<Integer> mCustomEarcons = new LinkedList<Integer>();
 173
 174    /**
 175     * List of resource identifiers for preference keys that correspond to
 176     * custom vibration patterns that should be played with an utterance.
 177     */
 178    private final List<Integer> mCustomVibrations = new LinkedList<Integer>();
 179
 180    /** Mapping from property name to property matcher. */
 181    private final HashMap<String, PropertyMatcher> mPropertyMatchers =
 182            new HashMap<String, PropertyMatcher>();
 183
 184    /** Filter for matching an event. */
 185    private final Filter mFilter;
 186
 187    /** Formatter for building an utterance. */
 188    private final Formatter mFormatter;
 189
 190    /** The index of this rule within the global rule list. */
 191    private final int mRuleIndex;
 192
 193    /** The context in which this speech rule operates. */
 194    private final Context mContext;
 195
 196    /**
 197     * The speech strategy defined the rule.
 198     * <p>
 199     * Note: This is either a resource name or a URI.
 200     * </p>
 201     */
 202    private final String mSpeechStrategy;
 203
 204    /**
 205     * The package targeted by this rule.
 206     */
 207    private String mPackageName;
 208
 209    /**
 210     * The location of the APK from which to load classes. This is required
 211     * since we need to load the plug-in classes through the TalkBack class
 212     * loader.
 213     */
 214    private final String mPublicSourceDir;
 215
 216    /**
 217     * The DOM node that defines this speech rule.
 218     */
 219    private final Node mNode;
 220
 221    /**
 222     * Creates a new speech rule that loads resources form the given
 223     * <code>context</code> and classes from the APK specified by the
 224     * <code>publicSourceDird</code>. The rule is defined in the
 225     * <code>speechStrategy</code> and is targeted to the
 226     * <code>packageName</code>. If the former is not provided resources are
 227     * loaded form the TalkBack context. If the latter is not provided class are
 228     * loaded from the current TalkBack APK. The speech rule content is loaded
 229     * from a DOM <code>node</code> and a <code>ruleIndex</code> is assigned to
 230     * the rule.
 231     * 
 232     * @throws IllegalStateException If the tries to load custom
 233     *             filter/formatter while <code>customInstancesSupported</code>
 234     *             is false;
 235     */
 236    private SpeechRule(Context context, String speechStrategy, String packageName,
 237            String publicSourceDird, Node node, int ruleIndex) {
 238        mContext = context;
 239        mSpeechStrategy = speechStrategy;
 240        mPackageName = packageName != null ? packageName : UNDEFINED_PACKAGE_NAME;
 241        mPublicSourceDir = publicSourceDird;
 242        mNode = node;
 243
 244        Filter filter = null;
 245        Formatter formatter = null;
 246
 247        // avoid call to Document#getNodesByTagName, it traverses the entire
 248        // document
 249        NodeList children = node.getChildNodes();
 250        for (int i = 0, count = children.getLength(); i < count; i++) {
 251            Node child = children.item(i);
 252
 253            if (child.getNodeType() != Node.ELEMENT_NODE) {
 254                continue;
 255            }
 256
 257            String nodeName = getUnqualifiedNodeName(child);
 258            if (NODE_NAME_METADATA.equalsIgnoreCase(nodeName)) {
 259                populateMetadata(child);
 260            } else if (NODE_NAME_FILTER.equals(nodeName)) {
 261                filter = createFilter(child);
 262            } else if (NODE_NAME_FORMATTER.equals(nodeName)) {
 263                formatter = createFormatter(child);
 264            }
 265        }
 266
 267        mFilter = filter;
 268        mFormatter = formatter;
 269        mRuleIndex = ruleIndex;
 270    }
 271
 272    /**
 273     * @return The XML representation of this rule.
 274     */
 275    public String asXmlString() {
 276        try {
 277            final TransformerFactory transformerFactory = TransformerFactory.newInstance();
 278            final Transformer transformer = transformerFactory.newTransformer();
 279            final DOMSource source = new DOMSource(mNode);
 280            final StringWriter writer = new StringWriter();
 281            final StreamResult result = new StreamResult(writer);
 282
 283            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
 284            transformer.transform(source, result);
 285
 286            return writer.toString();
 287        } catch (TransformerException e) {
 288            e.printStackTrace();
 289
 290            return "";
 291        }
 292    }
 293
 294    /**
 295     * @return The speech strategy that defined this rule.
 296     */
 297    public String getSpeechStrategy() {
 298        return mSpeechStrategy;
 299    }
 300
 301    /**
 302     * @return The package targeted by this rule.
 303     */
 304    public String getPackageName() {
 305        return mPackageName;
 306    }
 307
 308    /**
 309     * @return This rule's filter.
 310     */
 311    public Filter getFilter() {
 312        return mFilter;
 313    }
 314
 315    /**
 316     * @return This rule's formatter.
 317     */
 318    public Formatter getFormatter() {
 319        return mFormatter;
 320    }
 321
 322    /**
 323     * Applies this rule to an {@link AccessibilityEvent}. If the event is
 324     * accepted by the {@link Filter} the rule's {@link Formatter} is used to
 325     * populate a formatted {@link Utterance}.
 326     * 
 327     * @param event The event to which to apply the rule.
 328     * @param utterance Utterance to populate if the event is accepted.
 329     * @param filterArgs Addition arguments to the filter.
 330     * @param formatterArgs Addition arguments to the formatter.
 331     * @return True if the event matched the filter, false otherwise.
 332     */
 333    public boolean apply(AccessibilityEvent event, Utterance utterance, Bundle filterArgs,
 334            Bundle formatterArgs) {
 335        // No filter matches all events.
 336        if (mFilter != null && !mFilter.accept(event, mContext, filterArgs)) {
 337            return false;
 338        }
 339
 340        // No formatter means no utterance text.
 341        if (mFormatter != null && !mFormatter.format(event, mContext, utterance, formatterArgs)) {
 342            return false;
 343        }
 344
 345        utterance.getMetadata().putAll(mMetadata);
 346        utterance.getEarcons().addAll(mEarcons);
 347        utterance.getVibrationPatterns().addAll(mVibrationPatterns);
 348        utterance.getCustomEarcons().addAll(mCustomEarcons);
 349        utterance.getCustomVibrations().addAll(mCustomVibrations);
 350
 351        return true;
 352    }
 353
 354    /**
 355     * Populates the meta-data which determines how an {@link Utterance}
 356     * formatted by this rule should be announced.
 357     * 
 358     * @param node The meta-data node to parse.
 359     */
 360    private void populateMetadata(Node node) {
 361        final NodeList metadata = node.getChildNodes();
 362        final int count = metadata.getLength();
 363
 364        for (int i = 0; i < count; i++) {
 365            final Node child = metadata.item(i);
 366
 367            if (child.getNodeType() != Node.ELEMENT_NODE) {
 368                continue;
 369            }
 370
 371            final String unqualifiedName = getUnqualifiedNodeName(child);
 372            final String textContent = getTextContent(child);
 373
 374            if (PROPERTY_QUEUING.equals(unqualifiedName)) {
 375                final int mode = sQueueModeNameToQueueModeMap.get(textContent);
 376                mMetadata.putInt(unqualifiedName, mode);
 377            } else if (PROPERTY_EARCON.equals(unqualifiedName)) {
 378                final int resId = getResourceIdentifierContent(mContext, textContent);
 379                mEarcons.add(resId);
 380            } else if (PROPERTY_VIBRATION.equals(unqualifiedName)) {
 381                final int resId = getResourceIdentifierContent(mContext, textContent);
 382                mVibrationPatterns.add(resId);
 383            } else if (PROPERTY_CUSTOM_EARCON.equals(unqualifiedName)) {
 384                mCustomEarcons.add(getResourceIdentifierContent(mContext, textContent));
 385            } else if (PROPERTY_CUSTOM_VIBRATION.equals(unqualifiedName)) {
 386                mCustomVibrations.add(getResourceIdentifierContent(mContext, textContent));
 387            } else {
 388                final String value = (String) parsePropertyValue(unqualifiedName, textContent);
 389                mMetadata.putString(unqualifiedName, value);
 390            }
 391        }
 392    }
 393
 394    /**
 395     * Parses a property according to its expected type. Parsing failures are
 396     * logged and null is returned.
 397     * 
 398     * @param name The property name.
 399     * @param value The property value.
 400     * @return The parsed value or null if parse error occurs.
 401     */
 402    private static Comparable<?> parsePropertyValue(String name, String value) {
 403        if (PROPERTY_EVENT_TYPE.equals(name)) {
 404            return sEventTypeNameToValueMap.get(value);
 405        }
 406        if (isIntegerProperty(name)) {
 407            try {
 408                return Integer.parseInt(value);
 409            } catch (NumberFormatException nfe) {
 410                LogUtils.log(SpeechRule.class, Log.WARN, "Property: '%s' not interger. Ignoring!",
 411                        name);
 412                return null;
 413            }
 414        } else if (isFloatProperty(name)) {
 415            try {
 416                return Float.parseFloat(value);
 417            } catch (NumberFormatException nfe) {
 418                LogUtils.log(SpeechRule.class, Log.WARN, "Property: '%s' not float. Ignoring!",
 419                        name);
 420                return null;
 421            }
 422        } else if (isBooleanProperty(name)) {
 423            return Boolean.parseBoolean(value);
 424        } else if (isStringProperty(name)) {
 425            return value;
 426        } else {
 427            throw new IllegalArgumentException("Unknown property: " + name);
 428        }
 429    }
 430
 431    /**
 432     * Returns if a property is an integer.
 433     * 
 434     * @param propertyName The property name.
 435     * @return True if the property is an integer, false otherwise.
 436     */
 437    private static boolean isIntegerProperty(String propertyName) {
 438        return (PROPERTY_EVENT_TYPE.equals(propertyName)
 439                || PROPERTY_ITEM_COUNT.equals(propertyName)
 440                || PROPERTY_CURRENT_ITEM_INDEX.equals(propertyName)
 441                || PROPERTY_FROM_INDEX.equals(propertyName)
 442                || PROPERTY_TO_INDEX.equals(propertyName) || PROPERTY_SCROLL_X.equals(propertyName)
 443                || PROPERTY_SCROLL_Y.equals(propertyName)
 444                || PROPERTY_RECORD_COUNT.equals(propertyName)
 445                || PROPERTY_ADDED_COUNT.equals(propertyName)
 446                || PROPERTY_REMOVED_COUNT.equals(propertyName) || PROPERTY_QUEUING
 447                    .equals(propertyName))
 448                || PROPERTY_VERSION_CODE.equals(propertyName)
 449                || PROPERTY_PLATFORM_SDK.equals(propertyName);
 450    }
 451
 452    /**
 453     * Returns if a property is a float.
 454     * 
 455     * @param propertyName The property name.
 456     * @return True if the property is a float, false otherwise.
 457     */
 458    private static boolean isFloatProperty(String propertyName) {
 459        return PROPERTY_EVENT_TIME.equals(propertyName);
 460    }
 461
 462    /**
 463     * Returns if a property is a string.
 464     * 
 465     * @param propertyName The property name.
 466     * @return True if the property is a string, false otherwise.
 467     */
 468    private static boolean isStringProperty(String propertyName) {
 469        return (PROPERTY_PACKAGE_NAME.equals(propertyName)
 470                || PROPERTY_CLASS_NAME.equals(propertyName) || PROPERTY_TEXT.equals(propertyName)
 471                || PROPERTY_BEFORE_TEXT.equals(propertyName)
 472                || PROPERTY_CONTENT_DESCRIPTION.equals(propertyName)
 473                || PROPERTY_VERSION_NAME.equals(propertyName)
 474                || PROPERTY_PLATFORM_RELEASE.equals(propertyName));
 475    }
 476
 477    /**
 478     * Returns if a property is a boolean.
 479     * 
 480     * @param propertyName The property name.
 481     * @return True if the property is a boolean, false otherwise.
 482     */
 483    private static boolean isBooleanProperty(String propertyName) {
 484        return (PROPERTY_CHECKED.equals(propertyName) || PROPERTY_ENABLED.equals(propertyName)
 485                || PROPERTY_FULL_SCREEN.equals(propertyName)
 486                || PROPERTY_SCROLLABLE.equals(propertyName) || PROPERTY_PASSWORD
 487                    .equals(propertyName));
 488    }
 489
 490    /**
 491     * Create a {@link Filter} given a DOM <code>node</code>.
 492     * 
 493     * @param node The node.
 494     * @return The created filter.
 495     */
 496    private Filter createFilter(Node node) {
 497        NodeList children = node.getChildNodes();
 498
 499        // do we have a custom filter
 500        for (int i = 0, count = children.getLength(); i < count; i++) {
 501            Node child = children.item(i);
 502
 503            if (child.getNodeType() != Node.ELEMENT_NODE) {
 504                continue;
 505            }
 506
 507            String nodeName = getUnqualifiedNodeName(child);
 508            if (NODE_NAME_CUSTOM.equals(nodeName)) {
 509                return createNewInstance(getTextContent(child), Filter.class);
 510            }
 511        }
 512
 513        return new DefaultFilter(mContext, node);
 514    }
 515
 516    /**
 517     * Creates a {@link Formatter} given a DOM <code>node</code>.
 518     * 
 519     * @param node The node.
 520     * @return The created formatter.
 521     */
 522    private Formatter createFormatter(Node node) {
 523        NodeList children = node.getChildNodes();
 524
 525        for (int i = 0, count = children.getLength(); i < count; i++) {
 526            Node child = children.item(i);
 527
 528            if (child.getNodeType() != Node.ELEMENT_NODE) {
 529                continue;
 530            }
 531
 532            String nodeName = getUnqualifiedNodeName(child);
 533            if (NODE_NAME_CUSTOM.equals(nodeName)) {
 534                return createNewInstance(getTextContent(child), Formatter.class);
 535            }
 536        }
 537
 538        return new DefaultFormatter(node);
 539    }
 540
 541    /**
 542     * Creates a new instance given a <code>className</code> and the
 543     * <code>expectedClass</code> that instance must belong to.
 544     * 
 545     * @param className the class name.
 546     * @return New instance if succeeded, null otherwise.
 547     */
 548    @SuppressWarnings("unchecked")
 549    // the possible ClassCastException is handled by the method
 550            private
 551            <T> T createNewInstance(String className, Class<T> expectedClass) {
 552        try {
 553            Class<T> clazz = null;
 554            // if we are loaded by the context class loader => use the latter
 555            if (mContext.getClassLoader() == this.getClass().getClassLoader()) {
 556                clazz = (Class<T>) mContext.getClassLoader().loadClass(className);
 557            } else {
 558                // It is important to load the plug-in classes via the TalkBack
 559                // ClassLoader to achieve interoperability of the classes of the
 560                // different APKs. Form VM perspective a class loaded by two
 561                // different class loaders is considered as two separate
 562                // classes.
 563                DexFile dexFile = new DexFile(new File(mPublicSourceDir));
 564                ClassLoader classLoader = TalkBackService.asContext().getClassLoader();
 565                clazz = dexFile.loadClass(className, classLoader);
 566            }
 567            return clazz.newInstance();
 568        } catch (Exception e) {
 569            LogUtils.log(SpeechRule.class, Log.ERROR, "Rule: #%d. Could not load class: '%s'.",
 570                    mRuleIndex, className, e.toString());
 571        }
 572
 573        return null;
 574    }
 575
 576    /**
 577     * Factory method that creates all speech rules from the DOM representation
 578     * of a speechstrategy.xml. This class does not verify if the
 579     * <code>document</code> is well-formed and it is responsibility of the
 580     * client to do that.
 581     * 
 582     * @param context A {@link Context} instance for loading resources.
 583     * @param speechStrategy The speech strategy that defined the rules.
 584     * @param targetPackage The package targeted by the rules.
 585     * @param publicSourceDir The location of the plug-in APK for loading
 586     *            classes.
 587     * @param document The parsed XML.
 588     * @return The list of loaded speech rules.
 589     */
 590    public static ArrayList<SpeechRule> createSpeechRules(Context context, String speechStrategy,
 591            String targetPackage, String publicSourceDir, Document document)
 592            throws IllegalStateException {
 593        ArrayList<SpeechRule> speechRules = new ArrayList<SpeechRule>();
 594
 595        if (document == null || context == null) {
 596            return speechRules;
 597        }
 598
 599        NodeList children = document.getDocumentElement().getChildNodes();
 600        for (int i = 0, count = children.getLength(); i < count; i++) {
 601            Node child = children.item(i);
 602            if (child.getNodeType() == Node.ELEMENT_NODE) {
 603                try {
 604                    speechRules.add(new SpeechRule(context, speechStrategy, targetPackage,
 605                            publicSourceDir, child, i));
 606                } catch (IllegalStateException ise) {
 607                    LogUtils.log(SpeechRule.class, Log.WARN, "Failed loading speech rule: %s %s",
 608                            getTextContent(child), ise.toString());
 609                }
 610            }
 611        }
 612
 613        return speechRules;
 614    }
 615
 616    /**
 617     * Returns from the given <code>context</code> the text content of a
 618     * <code>node</code> after it has been localized.
 619     */
 620    private static String getLocalizedTextContent(Context context, Node node) {
 621        final String textContent = getTextContent(node);
 622        final int resId = getResourceIdentifierContent(context, textContent);
 623
 624        if (resId > 0) {
 625            return context.getString(resId);
 626        }
 627
 628        return textContent;
 629    }
 630
 631    /**
 632     * Returns a resource identifier from the given <code>context</code> for the
 633     * text content of a <code>node</code>.
 634     * <p>
 635     * Note: The resource identifier format is: @&lt;package
 636     * name&gt;:&lt;type&gt;/&lt;resource name&gt;
 637     * </p>
 638     */
 639    private static int getResourceIdentifierContent(Context context, String resName) {
 640        if (resName == null) {
 641            return 0;
 642        } else if (!resName.matches("@(\\w+:)?\\w+/\\w+")) {
 643            return 0;
 644        }
 645
 646        return context.getResources().getIdentifier(resName.substring(1), null,
 647                context.getPackageName());
 648    }
 649
 650    /**
 651     * Returns the text content of a given <code>node</code> by performing a
 652     * preorder traversal of the tree rooted at that node. </p> Note: Android
 653     * Java implementation is not compatible with Java 5.0 which provides such a
 654     * method.
 655     * 
 656     * @param node The node.
 657     * @return The text content.
 658     */
 659    private static String getTextContent(Node node) {
 660        StringBuilder builder = sTempBuilder;
 661        getTextContentRecursive(node, builder);
 662        String text = builder.toString();
 663        builder.delete(0, builder.length());
 664        return text;
 665    }
 666
 667    /**
 668     * Performs a recursive preorder traversal of a DOM tree and aggregating the
 669     * text content.
 670     * 
 671     * @param node The currently explored node.
 672     * @param builder Builder that aggregates the text content.
 673     */
 674    private static void getTextContentRecursive(Node node, StringBuilder builder) {
 675        NodeList children = node.getChildNodes();
 676        for (int i = 0, count = children.getLength(); i < count; i++) {
 677            Node child = children.item(i);
 678            if (child.getNodeType() == Node.TEXT_NODE) {
 679                builder.append(child.getNodeValue());
 680            }
 681            getTextContentRecursive(child, builder);
 682        }
 683    }
 684
 685    /**
 686     * Returns the unqualified <code>node</code> name i.e. without the prefix.
 687     * 
 688     * @param node The node.
 689     * @return The unqualified name.
 690     */
 691    private static String getUnqualifiedNodeName(Node node) {
 692        String nodeName = node.getNodeName();
 693        int colonIndex = nodeName.indexOf(COLON);
 694        if (colonIndex > -1) {
 695            nodeName = nodeName.substring(colonIndex + 1);
 696        }
 697        return nodeName;
 698    }
 699
 700    /**
 701     * Represents a default filter determining if the rule applies to a given
 702     * {@link AccessibilityEvent}.
 703     */
 704    private class DefaultFilter implements Filter {
 705        public DefaultFilter(Context context, Node node) {
 706            NodeList properties = node.getChildNodes();
 707
 708            for (int i = 0, count = properties.getLength(); i < count; i++) {
 709                Node child = properties.item(i);
 710
 711                if (child.getNodeType() != Node.ELEMENT_NODE) {
 712                    continue;
 713                }
 714
 715                String unqualifiedName = getUnqualifiedNodeName(child);
 716                String textContent = getTextContent(child);
 717                PropertyMatcher propertyMatcher =
 718                        new PropertyMatcher(context, unqualifiedName, textContent);
 719                mPropertyMatchers.put(unqualifiedName, propertyMatcher);
 720
 721                // if the speech rule specifies a target package we use that
 722                // value
 723                // rather the one passed as an argument to the rule constructor
 724                if (PROPERTY_PACKAGE_NAME.equals(unqualifiedName)) {
 725                    mPackageName = textContent;
 726                }
 727            }
 728        }
 729
 730        @Override
 731        public boolean accept(AccessibilityEvent event, Context context, Bundle args) {
 732            // the order here matters and is from most frequently used to less
 733            PropertyMatcher eventTypeMatcher = mPropertyMatchers.get(PROPERTY_EVENT_TYPE);
 734            if (eventTypeMatcher != null) {
 735                int eventType = event.getEventType();
 736                if (!eventTypeMatcher.accept(eventType)) {
 737                    return false;
 738                }
 739            }
 740            PropertyMatcher packageNameMatcher = mPropertyMatchers.get(PROPERTY_PACKAGE_NAME);
 741            if (packageNameMatcher != null) {
 742                CharSequence packageName = event.getPackageName();
 743                if (!packageNameMatcher.accept(packageName)) {
 744                    return false;
 745                }
 746            }
 747            // special case
 748            PropertyMatcher classNameMatcher = mPropertyMatchers.get(PROPERTY_CLASS_NAME);
 749            if (classNameMatcher != null) {
 750                CharSequence eventClass = event.getClassName();
 751                CharSequence eventPackage = event.getPackageName();
 752                String filteringPackage = null;
 753                if (packageNameMatcher != null) {
 754                    filteringPackage = (String) packageNameMatcher.getAcceptedValues()[0];
 755                }
 756                if (!classNameMatcher.accept(eventClass, eventPackage, filteringPackage)) {
 757                    return false;
 758                }
 759            }
 760            PropertyMatcher textMatcher = mPropertyMatchers.get(PROPERTY_TEXT);
 761            if (textMatcher != null) {
 762                CharSequence eventText = Utils.getEventText(context, event);
 763                if (!textMatcher.accept(eventText)) {
 764                    return false;
 765                }
 766            }
 767            PropertyMatcher beforeTextMatcher = mPropertyMatchers.get(PROPERTY_BEFORE_TEXT);
 768            if (beforeTextMatcher != null) {
 769                CharSequence beforeText = event.getBeforeText();
 770                if (!beforeTextMatcher.accept(beforeText)) {
 771                    return false;
 772                }
 773            }
 774            PropertyMatcher contentDescriptionMatcher =
 775                    mPropertyMatchers.get(PROPERTY_CONTENT_DESCRIPTION);
 776            if (contentDescriptionMatcher != null) {
 777                CharSequence contentDescription = event.getContentDescription();
 778                if (!contentDescriptionMatcher.accept(contentDescription)) {
 779                    return false;
 780                }
 781            }
 782            PropertyMatcher eventTimeMatcher = mPropertyMatchers.get(PROPERTY_EVENT_TIME);
 783            if (eventTimeMatcher != null) {
 784                long eventTime = event.getEventTime();
 785                if (!eventTimeMatcher.accept(eventTime)) {
 786                    return false;
 787                }
 788            }
 789            PropertyMatcher itemCountMatcher = mPropertyMatchers.get(PROPERTY_ITEM_COUNT);
 790            if (itemCountMatcher != null) {
 791                int itemCount = event.getItemCount();
 792                if (!itemCountMatcher.accept(itemCount)) {
 793                    return false;
 794                }
 795            }
 796            PropertyMatcher currentItemIndexMatcher =
 797                    mPropertyMatchers.get(PROPERTY_CURRENT_ITEM_INDEX);
 798            if (currentItemIndexMatcher != null) {
 799                int currentItemIndex = event.getCurrentItemIndex();
 800                if (!currentItemIndexMatcher.accept(currentItemIndex)) {
 801                    return false;
 802                }
 803            }
 804            PropertyMatcher fromIndexMatcher = mPropertyMatchers.get(PROPERTY_FROM_INDEX);
 805            if (fromIndexMatcher != null) {
 806                int fromIndex = event.getFromIndex();
 807                if (!fromIndexMatcher.accept(fromIndex)) {
 808                    return false;
 809                }
 810            }
 811            PropertyMatcher toIndexMatcher = mPropertyMatchers.get(PROPERTY_TO_INDEX);
 812            if (toIndexMatcher != null) {
 813                int toIndex = event.getToIndex();
 814                if (!toIndexMatcher.accept(toIndex)) {
 815                    return false;
 816                }
 817            }
 818            PropertyMatcher scrollableMatcher = mPropertyMatchers.get(PROPERTY_SCROLLABLE);
 819            if (scrollableMatcher != null) {
 820                boolean scrollable = event.isScrollable();
 821                if (!scrollableMatcher.accept(scrollable)) {
 822                    return false;
 823                }
 824            }
 825            PropertyMatcher scrollXMatcher = mPropertyMatchers.get(PROPERTY_SCROLL_X);
 826            if (scrollXMatcher != null) {
 827                int scrollX = event.getScrollX();
 828                if (!scrollXMatcher.accept(scrollX)) {
 829                    return false;
 830                }
 831            }
 832            PropertyMatcher scrollYMatcher = mPropertyMatchers.get(PROPERTY_SCROLL_Y);
 833            if (scrollYMatcher != null) {
 834                int scrollY = event.getScrollY();
 835                if (!scrollYMatcher.accept(scrollY)) {
 836                    return false;
 837                }
 838            }
 839            PropertyMatcher recordCountMatcher = mPropertyMatchers.get(PROPERTY_RECORD_COUNT);
 840            if (recordCountMatcher != null) {
 841                int recordCount = event.getRecordCount();
 842                if (!recordCountMatcher.accept(recordCount)) {
 843                    return false;
 844                }
 845            }
 846            PropertyMatcher isCheckedMatcher = mPropertyMatchers.get(PROPERTY_CHECKED);
 847            if (isCheckedMatcher != null) {
 848                boolean isChecked = event.isChecked();
 849                if (!isCheckedMatcher.accept(isChecked)) {
 850                    return false;
 851                }
 852            }
 853            PropertyMatcher isEnabledMatcher = mPropertyMatchers.get(PROPERTY_ENABLED);
 854            if (isEnabledMatcher != null) {
 855                boolean isEnabled = event.isEnabled();
 856                if (!isEnabledMatcher.accept(isEnabled)) {
 857                    return false;
 858                }
 859            }
 860            PropertyMatcher isFullScreenMatcher = mPropertyMatchers.get(PROPERTY_FULL_SCREEN);
 861            if (isFullScreenMatcher != null) {
 862                boolean isFullScreen = event.isFullScreen();
 863                if (!isFullScreenMatcher.accept(isFullScreen)) {
 864                    return false;
 865                }
 866            }
 867            PropertyMatcher isPasswordMatcher = mPropertyMatchers.get(PROPERTY_PASSWORD);
 868            if (isPasswordMatcher != null) {
 869                boolean isPassword = event.isPassword();
 870                if (!isPasswordMatcher.accept(isPassword)) {
 871                    return false;
 872                }
 873            }
 874            PropertyMatcher addedCountMatcher = mPropertyMatchers.get(PROPERTY_ADDED_COUNT);
 875            if (addedCountMatcher != null) {
 876                int addedCount = event.getAddedCount();
 877                if (!addedCountMatcher.accept(addedCount)) {
 878                    return false;
 879                }
 880            }
 881            PropertyMatcher removedCountMatcher = mPropertyMatchers.get(PROPERTY_REMOVED_COUNT);
 882            if (removedCountMatcher != null) {
 883                int removedCount = event.getRemovedCount();
 884                if (!removedCountMatcher.accept(removedCount)) {
 885                    return false;
 886                }
 887            }
 888            PropertyMatcher versionCodeMatcher = mPropertyMatchers.get(PROPERTY_VERSION_CODE);
 889            if (versionCodeMatcher != null) {
 890                String packageName = event.getPackageName().toString();
 891                int versionCode = Utils.getVersionCode(context, packageName);
 892                if (!versionCodeMatcher.accept(versionCode)) {
 893                    return false;
 894                }
 895            }
 896            PropertyMatcher versionNameMatcher = mPropertyMatchers.get(PROPERTY_VERSION_NAME);
 897            if (versionNameMatcher != null) {
 898                String packageName = event.getPackageName().toString();
 899                String versionName = Utils.getVersionName(context, packageName);
 900                if (!versionNameMatcher.accept(versionName)) {
 901                    return false;
 902                }
 903            }
 904            PropertyMatcher platformReleaseMatcher =
 905                    mPropertyMatchers.get(PROPERTY_PLATFORM_RELEASE);
 906            if (platformReleaseMatcher != null) {
 907                String platformRelease = Build.VERSION.RELEASE;
 908                if (!platformReleaseMatcher.accept(platformRelease)) {
 909                    return false;
 910                }
 911            }
 912            PropertyMatcher platformSdkMatcher = mPropertyMatchers.get(PROPERTY_PLATFORM_SDK);
 913            if (platformSdkMatcher != null) {
 914                int platformSdk = Build.VERSION.SDK_INT;
 915                if (!platformSdkMatcher.accept(platformSdk)) {
 916                    return false;
 917                }
 918            }
 919            return true;
 920        }
 921    }
 922
 923    /**
 924     * This class is default formatter for building utterance for announcing an
 925     * {@link AccessibilityEvent}. The formatting strategy is to populate a
 926     * template with event properties or strings selected via a regular
 927     * expression from the event's text. If no template is provided the
 928     * properties or regular expression selections are concatenated with space
 929     * as delimiter.
 930     */
 931    private class DefaultFormatter implements Formatter {
 932
 933        // node names
 934
 935        private static final String NODE_NAME_TEMPLATE = "template";
 936
 937        private static final String NODE_NAME_PROPERTY = "property";
 938
 939        /**
 940         * optional template to populate with selected values
 941         */
 942        private final String mTemplate;
 943
 944        private final List<Pair<String, String>> mSelectors;
 945
 946        /**
 947         * Creates a new formatter from a given DOM {@link Node}.
 948         * 
 949         * @param node The node.
 950         */
 951        public DefaultFormatter(Node node) {
 952            mSelectors = new ArrayList<Pair<String, String>>();
 953            String template = null;
 954
 955            NodeList children = node.getChildNodes();
 956            for (int i = 0, count = children.getLength(); i < count; i++) {
 957                Node child = children.item(i);
 958
 959                if (child.getNodeType() != Node.ELEMENT_NODE) {
 960                    continue;
 961                }
 962
 963                String unqualifiedName = getUnqualifiedNodeName(child);
 964                // some elements contain mandatory reference to a string
 965                // resource
 966                if (NODE_NAME_TEMPLATE.equals(unqualifiedName)) {
 967                    template = getLocalizedTextContent(mContext, child);
 968                } else if (NODE_NAME_PROPERTY.equals(unqualifiedName)) {
 969                    mSelectors.add(new Pair<String, String>(unqualifiedName,
 970                            getLocalizedTextContent(mContext, child)));
 971                } else {
 972                    mSelectors
 973                            .add(new Pair<String, String>(unqualifiedName, getTextContent(child)));
 974                }
 975            }
 976
 977            mTemplate = template;
 978        }
 979
 980        @Override
 981        public boolean format(AccessibilityEvent event, Context context, Utterance utterance,
 982                Bundle args) {
 983            List<Pair<String, String>> selectors = mSelectors;
 984            Object[] arguments = new Object[selectors.size()];
 985
 986            for (int i = 0, count = selectors.size(); i < count; i++) {
 987                Pair<String, String> selector = selectors.get(i);
 988                String selectorType = selector.first;
 989                String selectorValue = selector.second;
 990
 991                if (NODE_NAME_PROPERTY.equals(selectorType)) {
 992                    Object propertyValue = getPropertyValue(selectorValue, event);
 993                    arguments[i] = propertyValue != null ? propertyValue : "";
 994                } else {
 995                    throw new IllegalArgumentException("Unknown selector type: [" + selector.first
 996                            + selector.second + "]");
 997                }
 998            }
 999
1000            utterance.getEarcons().addAll(mEarcons);
1001            utterance.getVibrationPatterns().addAll(mVibrationPatterns);
1002
1003            formatTemplateOrAppendSpaceSeparatedValueIfNoTemplate(utterance, arguments);
1004            
1005            return true;
1006        }
1007
1008        /**
1009         * Returns the value of a given <code>property</code> of an
1010         * <code>event</code>.
1011         * 
1012         * @param property The property
1013         * @param event The event.
1014         * @return the value.
1015         */
1016        private Object getPropertyValue(String property, AccessibilityEvent event) {
1017            // no reflection to avoid performance hit
1018            if (PROPERTY_EVENT_TYPE.equals(property)) {
1019                return event.getEventType();
1020            } else if (PROPERTY_PACKAGE_NAME.equals(property)) {
1021                return event.getPackageName();
1022            } else if (PROPERTY_CLASS_NAME.equals(property)) {
1023                return event.getClassName();
1024            } else if (PROPERTY_TEXT.equals(property)) {
1025                return Utils.getEventText(mContext, event);
1026            } else if (PROPERTY_BEFORE_TEXT.equals(property)) {
1027                return event.getBeforeText();
1028            } else if (PROPERTY_CONTENT_DESCRIPTION.equals(property)) {
1029                return event.getContentDescription();
1030            } else if (PROPERTY_EVENT_TIME.equals(property)) {
1031                return event.getEventTime();
1032            } else if (PROPERTY_ITEM_COUNT.equals(property)) {
1033                return event.getItemCount();
1034            } else if (PROPERTY_CURRENT_ITEM_INDEX.equals(property)) {
1035                return event.getCurrentItemIndex();
1036            } else if (PROPERTY_FROM_INDEX.equals(property)) {
1037                return event.getFromIndex();
1038            } else if (PROPERTY_TO_INDEX.equals(property)) {
1039                return event.getToIndex();
1040            } else if (PROPERTY_SCROLLABLE.equals(property)) {
1041                return event.isScrollable();
1042            } else if (PROPERTY_SCROLL_X.equals(property)) {
1043                return event.getScrollX();
1044            } else if (PROPERTY_SCROLL_Y.equals(property)) {
1045                return event.getScrollY();
1046            } else if (PROPERTY_RECORD_COUNT.equals(property)) {
1047                return event.getRecordCount();
1048            } else if (PROPERTY_CHECKED.equals(property)) {
1049                return event.isChecked();
1050            } else if (PROPERTY_ENABLED.equals(property)) {
1051                return event.isEnabled();
1052            } else if (PROPERTY_FULL_SCREEN.equals(property)) {
1053                return event.isFullScreen();
1054            } else if (PROPERTY_PASSWORD.equals(property)) {
1055                return event.isPassword();
1056            } else if (PROPERTY_ADDED_COUNT.equals(property)) {
1057                return event.getAddedCount();
1058            } else if (PROPERTY_REMOVED_COUNT.equals(property)) {
1059                return event.getRemovedCount();
1060            } else {
1061                throw new IllegalArgumentException("Unknown property : " + property);
1062            }
1063        }
1064
1065        /**
1066         * Replaces template parameters in the template for this
1067         * {@link SpeechRule} with the <code>arguments</code> in case such a
1068         * template was provided. If no template was provided the arguments are
1069         * concatenated using space as delimiter. The <code>utterance</code> is
1070         * populated with the generated text.
1071         * 
1072         * @param utterance The builder to which to append the utterance.
1073         * @param arguments The formatting arguments.
1074         */
1075        private void formatTemplateOrAppendSpaceSeparatedValueIfNoTemplate(Utterance utterance,
1076                Object[] arguments) {
1077            final StringBuilder utteranceText = utterance.getText();
1078
1079            if (mTemplate != null) {
1080                try {
1081                    final String formatted = String.format(mTemplate, arguments);
1082                    utteranceText.append(formatted);
1083                } catch (MissingFormatArgumentException mfae) {
1084                    LogUtils.log(DefaultFormatter.class, Log.ERROR, "Speech rule: '%d' has "
1085                            + "inconsistency between template: '%s' and arguments: '%s'. "
1086                            + "Possibliy #template arguments does not " + "match #parameters. %s",
1087                            mRuleIndex, mTemplate, arguments, mfae.toString());
1088                }
1089            } else {
1090                for (Object arg : arguments) {
1091                    utteranceText.append(arg);
1092                    utteranceText.append(SPACE);
1093                }
1094                if (utteranceText.length() > 0) {
1095                    utteranceText.deleteCharAt(utteranceText.length() - 1);
1096                }
1097            }
1098        }
1099    }
1100
1101    /**
1102     * Helper class for matching properties.
1103     */
1104    private static class PropertyMatcher {
1105
1106        /**
1107         * Match type if a property is equal to an expected value.
1108         */
1109        private static final int TYPE_EQUALS = 0;
1110
1111        /**
1112         * Match type if a numeric property is less than or equal to an expected
1113         * value.
1114         */
1115        private static final int TYPE_LESS_THAN_OR_EQUAL = 1;
1116
1117        /**
1118         * Match type if a numeric property is greater than or equal to an
1119         * expected value.
1120         */
1121        private static final int TYPE_GREATER_THAN_OR_EQUAL = 2;
1122
1123        /**
1124         * Match type if a numeric property is less than an expected value.
1125         */
1126        private static final int TYPE_LESS_THAN = 3;
1127
1128        /**
1129         * Match type if a numeric property is greater than an expected value.
1130         */
1131        private static final int TYPE_GREATER_THAN = 4;
1132
1133        /**
1134         * Match type if a numeric property is equal to one of several expected
1135         * values.
1136         */
1137        private static final int TYPE_OR = 5;
1138
1139        /**
1140         * String for a regex pattern than matches float numbers.
1141         */
1142        private static final String PATTERN_STRING_FLOAT =
1143                "(\\s)*([+-])?((\\d)+(\\.(\\d)+)?|\\.(\\d)+)(\\s)*";
1144
1145        /**
1146         * Pattern to match a less than or equal a numeric value constraint.
1147         */
1148        private static final Pattern PATTERN_LESS_THAN_OR_EQUAL = Pattern.compile("(\\s)*<="
1149                + PATTERN_STRING_FLOAT);
1150
1151        /**
1152         * Pattern to match a greater than or equal a numeric value constraint.
1153         */
1154        private static final Pattern PATTERN_GREATER_THAN_OR_EQUAL = Pattern.compile("(\\s)*>="
1155                + PATTERN_STRING_FLOAT);
1156
1157        /**
1158         * Pattern to match a less than a numeric value constraint.
1159         */
1160        private static final Pattern PATTERN_LESS_THAN = Pattern.compile("(\\s)*<"
1161                + PATTERN_STRING_FLOAT);
1162
1163        /**
1164         * Pattern to match a greater than a numeric value constraint.
1165         */
1166        private static final Pattern PATTERN_GREATER_THAN = Pattern.compile("(\\s)*>"
1167                + PATTERN_STRING_FLOAT);
1168
1169        /**
1170         * Pattern to match an or constraint.
1171         */
1172        private static final Pattern PATTERN_OR = Pattern.compile("(.)+\\|\\|(.)+(\\|\\|(.)+)*");
1173
1174        /**
1175         * Pattern for splitting an or constraint into simple equal constraints.
1176         */
1177        private static final Pattern PATTERN_SPLIT_OR = Pattern.compile("(\\s)*\\|\\|(\\s)*");
1178
1179        /**
1180         * The less than or equal string.
1181         */
1182        private static final String LESS_THAN_OR_EQUAL = "<=";
1183
1184        /**
1185         * The greater than or equal string.
1186         */
1187        private static final String GREATER_THAN_OR_EQUAL = ">=";
1188
1189        /**
1190         * The less than string.
1191         */
1192        private static final String LESS_THAN = "<";
1193
1194        /**
1195         * The greater than string.
1196         */
1197        private static final String GREATER_THAN = ">";
1198
1199        /**
1200         * The name of the property matched by this instance.
1201         */
1202        private String mPropertyName;
1203
1204        /**
1205         * The type of matching to be performed.
1206         */
1207        private int mType;
1208
1209        /**
1210         * The values accepted by this matcher.
1211         */
1212        private Object[] mAcceptedValues;
1213
1214        /**
1215         * Context handled for accessing resources.
1216         */
1217        private Context mContext;
1218
1219        /**
1220         * Creates a new instance.
1221         * 
1222         * @param context The Context for accessing resources.
1223         * @param propertyName The name of the matched property.
1224         * @param acceptedValue The not parsed accepted value.
1225         */
1226        public PropertyMatcher(Context context, String propertyName, String acceptedValue) {
1227            mContext = context;
1228            mPropertyName = propertyName;
1229            if (acceptedValue == null) {
1230                return;
1231            }
1232            if ((isIntegerProperty(propertyName) || isFloatProperty(propertyName))
1233                    && PATTERN_LESS_THAN_OR_EQUAL.matcher(acceptedValue).matches()) {
1234                mType = TYPE_LESS_THAN_OR_EQUAL;
1235                int fromIndex = acceptedValue.indexOf(LESS_THAN_OR_EQUAL);
1236                String valueString = acceptedValue.substring(fromIndex + 2).trim();
1237                mAcceptedValues = new Object[] {
1238                    parseProper

Large files files are truncated, but you can click here to view the full file