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