PageRenderTime 40ms CodeModel.GetById 8ms RepoModel.GetById 0ms app.codeStats 0ms

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

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