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

http://eyes-free.googlecode.com/ · Java · 584 lines · 302 code · 59 blank · 223 comment · 40 complexity · 38c544776e61f01ee45cf343ef9beb18 MD5 · raw file

  1. /*
  2. * Copyright (C) 2010 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 com.google.android.marvin.talkback.TalkBackService.InfrastructureStateListener;
  18. import android.content.ComponentName;
  19. import android.content.Context;
  20. import android.content.Intent;
  21. import android.content.SharedPreferences;
  22. import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
  23. import android.content.pm.PackageManager;
  24. import android.content.pm.ResolveInfo;
  25. import android.content.pm.ServiceInfo;
  26. import android.content.pm.PackageManager.NameNotFoundException;
  27. import android.content.res.Resources;
  28. import android.os.Handler;
  29. import android.os.Looper;
  30. import android.os.Message;
  31. import android.os.Handler.Callback;
  32. import android.preference.PreferenceManager;
  33. import android.util.Log;
  34. import java.util.ArrayList;
  35. import java.util.HashMap;
  36. import java.util.Iterator;
  37. import java.util.List;
  38. import java.util.regex.Pattern;
  39. /**
  40. * This class is responsible for managing TalkBack plug-ins.
  41. * <p>
  42. * TalkBack plug-in mechanism:
  43. * </p>
  44. * <ul>
  45. * <li>
  46. * A plug-in is a standard Android APK file.
  47. * </li>
  48. * <li>Each plug-in consists of one or more speech strategy files, zero or more
  49. * custom formatters/filters, and a place holder service.
  50. * </li>
  51. * <li>Each plug-in is targeted for one or more packages which means that it
  52. * defines rules for speaking events originating from these packages.
  53. * </li>
  54. * <li>If more than one plug-in targets the same package the last loaded one
  55. * wins i.e. overrides previously loaded ones.
  56. * </li>
  57. * <li>The plug-in mechanism watches for (re)installed plug-ins and loads them
  58. * appropriately.
  59. * </li>
  60. * <li>
  61. * <strong>
  62. * For a plug-in to define custom filters and formatters it must have the
  63. * com.google.android.marvin.talkback.PERMISSION_PLUGIN_DEFINES_CLASSES
  64. * permission which is signature protected. In other words, only packages
  65. * signed with the same key with TalkBack will be able to define custom
  66. * filters and formatters.
  67. * </strong>
  68. * </li>
  69. * </ul>
  70. * <p>
  71. * Android.apk:
  72. * </p>
  73. * <ul>
  74. * <li>
  75. * The place holder service should be declared as such in the manifest and
  76. * it should also have intent filer for action
  77. * <code>"com.google.android.marvin.talkback.Plugin"</code>.
  78. * </li>
  79. * <li>
  80. * The place holder service declaration should contain two meta data
  81. * declarations. The declaration <code>packages</code> is a colon
  82. * separated list of package names and denotes the packages this
  83. * plug-in is targeted for and therefore will receive only events
  84. * from these packages. The declaration <code>speechstrategies</code>
  85. * is a colon separated list of XML file names which define the
  86. * speech strategies for the handled packages. <strong> Note that
  87. * the first speech strategy should contain the rules for the first
  88. * package and so on. Excessive package names or speech strategies
  89. * are ignored.
  90. * </li>
  91. * </ul>
  92. * <p>
  93. * Speech strategies:
  94. * </p>
  95. * <ul>
  96. * <li>
  97. * Each speech strategy XML file contains rules for handling a single
  98. * package. The speech rules in this files can refer to both resources
  99. * and Java classes which implement a filter or formatter interfaces.
  100. * </li>
  101. * <li>
  102. * Speech strategies <strong>should be placed in the res/raw folder</strong>.
  103. * </li>
  104. * <li>
  105. * The plug-ins' speech rules override the default TalkBack rules.
  106. * </li>
  107. * <li>
  108. * Speech rules are examined linearly and the first matching one is
  109. * used, therefore their ordering in the speech strategy file matters.
  110. * </li>
  111. * <p>
  112. * Custom filters/formatters:
  113. * </p>
  114. * <ul>
  115. * <li>
  116. * Are implementations of <code>com.google.android.marvin.talkback.Filter</code>
  117. * and <code>com.google.android.marvin.talkback.Formatter</code> and used when
  118. * the expressiveness of the XML is insufficient for proper event filtering or
  119. * utterance formatting.
  120. * </li>
  121. * <li>
  122. * </li>
  123. * </ul>
  124. *
  125. * @author svetoslavganov@google.com (Svetoslav R. Ganov)
  126. */
  127. public class PluginManager implements Handler.Callback, InfrastructureStateListener,
  128. OnSharedPreferenceChangeListener {
  129. /**
  130. * Tag used for logging.
  131. */
  132. private static final String LOG_TAG = "PluginManager";
  133. /**
  134. * Mutex lock to ensure atomic operation while caching plug-ins.
  135. */
  136. private static final Object mLock = new Object();
  137. /**
  138. * This action classifies a service as a TalkBack plug-in.
  139. */
  140. private static final String ACTION_TALKBACK_PLUGIN = "com.google.android.marvin.talkback.Plugin";
  141. /**
  142. * Meta-data key to obtain packages handled by a given plug-in.
  143. */
  144. private static final String KEY_METADATA_PACKAGES = "packages";
  145. /**
  146. * Meta-data key to obtain speech strategies provided by a given plug-in.
  147. */
  148. private static final String KEY_METADATA_SPEECHSTRATEGIES = "speechstrategies";
  149. /**
  150. * Intent for fetching TalkBack plug-ins.
  151. */
  152. private static final Intent sPluginIntent = new Intent(ACTION_TALKBACK_PLUGIN);
  153. /**
  154. * Message to perform asynchronous plug-in loading.
  155. */
  156. private static final int WHAT_DO_LOAD_PLUGIN_FROM_HANDLER = 0x00000001;
  157. /**
  158. * Mapping from a plug-in package target packages for removing obsolete speech rules.
  159. */
  160. private final HashMap<String, PluginInfo> mPluginCache = new HashMap<String, PluginInfo>();
  161. /**
  162. * Pre-compiles a pattern for splitting colon separated strings.
  163. */
  164. private final Pattern mColonSplitPattern = Pattern.compile(":");
  165. /**
  166. * The {@link Context} this manager operates in.
  167. */
  168. private final Context mContext;
  169. /**
  170. * Handle to the processor for managing speech rules.
  171. */
  172. private final SpeechRuleProcessor mSpeechRuleProcessor;
  173. /**
  174. * Handle to the package manager for plug-in loading.
  175. */
  176. private final PackageManager mPackageManager;
  177. /**
  178. * Handle to the shared preferences.
  179. */
  180. private final SharedPreferences mPreferences;
  181. /**
  182. * Monitor to handle add/remove/change of packages.
  183. */
  184. private final BasePackageMonitor mPackageMonitor;
  185. /**
  186. * Worker for asynchronous plug-in loading.
  187. */
  188. private final Worker mWorker;
  189. /**
  190. * Creates a new instance.
  191. */
  192. PluginManager(Context context, SpeechRuleProcessor processor) {
  193. mContext = context;
  194. mSpeechRuleProcessor = processor;
  195. mPackageManager = context.getPackageManager();
  196. mPreferences = PreferenceManager.getDefaultSharedPreferences(context);
  197. mPackageMonitor = new BasePackageMonitor() {
  198. @Override
  199. protected void onPackageAdded(String packageName) {
  200. handlePackageAddedOrChanged(packageName);
  201. }
  202. @Override
  203. protected void onPackageChanged(String packageName) {
  204. handlePackageAddedOrChanged(packageName);
  205. }
  206. @Override
  207. protected void onPackageRemoved(String packageName) {
  208. handlePackageRemoved(packageName);
  209. }
  210. };
  211. mWorker = new Worker(this);
  212. }
  213. /**
  214. * @return The list of plug-ins fetched via the given <code>packageManager</code>.
  215. */
  216. public static List<ServiceInfo> getPlugins(PackageManager packageManager) {
  217. List<ResolveInfo> resolveInfos = packageManager.queryIntentServices(sPluginIntent,
  218. PackageManager.GET_META_DATA);
  219. ArrayList<ServiceInfo> plugins = new ArrayList<ServiceInfo>();
  220. for (ResolveInfo resolveInfo : resolveInfos) {
  221. plugins.add(resolveInfo.serviceInfo);
  222. }
  223. return plugins;
  224. }
  225. public boolean handleMessage(Message message) {
  226. if (message.what == WHAT_DO_LOAD_PLUGIN_FROM_HANDLER) {
  227. doLoadPluginFromHandler((PluginInfo) message.obj);
  228. return true;
  229. }
  230. return false;
  231. }
  232. public void onInfrastructureStateChange(boolean isInitialized) {
  233. if (isInitialized) {
  234. buildPluginCache();
  235. mPreferences.registerOnSharedPreferenceChangeListener(this);
  236. mPackageMonitor.register(mContext);
  237. mWorker.start();
  238. loadPlugins();
  239. } else {
  240. clearPluginCache();
  241. mPreferences.unregisterOnSharedPreferenceChangeListener(this);
  242. mPackageMonitor.unregister();
  243. mWorker.stop();
  244. unloadPlugins();
  245. }
  246. }
  247. public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
  248. Object value = null;
  249. synchronized (mLock) {
  250. value = mPluginCache.get(key);
  251. }
  252. if (value != null && value instanceof PluginInfo) {
  253. PluginInfo pluginInfo = (PluginInfo) value;
  254. pluginInfo.enabled = sharedPreferences.getBoolean(key, false);
  255. if (pluginInfo.enabled) {
  256. loadPlugin(pluginInfo);
  257. } else {
  258. unloadPlugin(pluginInfo);
  259. }
  260. }
  261. }
  262. /**
  263. * Builds the plug-in cache.
  264. */
  265. private void buildPluginCache() {
  266. synchronized (mLock) {
  267. List<ServiceInfo> plugins = getPlugins(mPackageManager);
  268. for (ServiceInfo plugin : plugins) {
  269. if (!isValidPlugin(plugin)) {
  270. continue;
  271. }
  272. String key = new ComponentName(plugin.packageName, plugin.name).flattenToString();
  273. PluginInfo pluginInfo = createPluginInfo(plugin);
  274. mPluginCache.put(key, pluginInfo);
  275. }
  276. }
  277. }
  278. /**
  279. * Clears the plug-in cache.
  280. */
  281. private void clearPluginCache() {
  282. synchronized (mLock) {
  283. mPluginCache.clear();
  284. }
  285. }
  286. /**
  287. * @return If a <code>plug-in</code> is valid which is it has the same
  288. * shared user Id (this requires that the plug-in package is
  289. * signed with the TalkBack key).
  290. */
  291. private boolean isValidPlugin(ServiceInfo plugin) {
  292. return (android.os.Process.myUid() == plugin.applicationInfo.uid);
  293. }
  294. /**
  295. * Loads the plug-ins.
  296. */
  297. private void loadPlugins() {
  298. synchronized (mLock) {
  299. for (PluginInfo pluginInfo : mPluginCache.values()) {
  300. if (pluginInfo.enabled) {
  301. loadPlugin(pluginInfo);
  302. }
  303. }
  304. }
  305. }
  306. /**
  307. * Unloads the plug-ins.
  308. */
  309. private void unloadPlugins() {
  310. for (PluginInfo pluginInfo : mPluginCache.values()) {
  311. if (pluginInfo.enabled) {
  312. unloadPlugin(pluginInfo);
  313. }
  314. }
  315. }
  316. /**
  317. * @return A plug-in info instance given the <code>plug-in</code>.
  318. */
  319. private PluginInfo createPluginInfo(ServiceInfo plugin) {
  320. String targePackagesValue = plugin.metaData.getString(KEY_METADATA_PACKAGES);
  321. String speechStrategiesValue = plugin.metaData.getString(KEY_METADATA_SPEECHSTRATEGIES);
  322. String key = new ComponentName(plugin.packageName, plugin.name).flattenToString();
  323. PluginInfo info = new PluginInfo();
  324. info.enabled = mPreferences.getBoolean(key, false);
  325. info.serviceInfo = plugin;
  326. info.publicSourceDir = plugin.applicationInfo.publicSourceDir;
  327. info.speechStrategies = mColonSplitPattern.split(speechStrategiesValue);
  328. info.targetPackages = mColonSplitPattern.split(targePackagesValue);
  329. return info;
  330. }
  331. /**
  332. * Loads a given <code>pluginInfo</code>.
  333. */
  334. private void loadPlugin(PluginInfo pluginInfo) {
  335. String key = new ComponentName(pluginInfo.serviceInfo.packageName,
  336. pluginInfo.serviceInfo.name).flattenToString();
  337. if (!mPreferences.getBoolean(key, false)) {
  338. return;
  339. }
  340. mWorker.getHandler().obtainMessage(WHAT_DO_LOAD_PLUGIN_FROM_HANDLER, pluginInfo)
  341. .sendToTarget();
  342. }
  343. /**
  344. * Unloads a given <code>pluginInfo</code>.
  345. */
  346. private void unloadPlugin(PluginInfo pluginInfo) {
  347. String[] targetPackages = pluginInfo.targetPackages;
  348. for (int i = 0, count = targetPackages.length; i < count; i++) {
  349. mSpeechRuleProcessor.removeSpeechRulesForPackage(targetPackages[i]);
  350. }
  351. }
  352. /**
  353. * Loads a <code>plugin</code> and should be called from our handler
  354. * to achieve asynchronous loading.
  355. */
  356. private void doLoadPluginFromHandler(PluginInfo pluginInfo) {
  357. String[] targetPackages = pluginInfo.targetPackages;
  358. String[] speechStrategies = pluginInfo.speechStrategies;
  359. String pluginPackage = pluginInfo.serviceInfo.packageName;
  360. Context context = createPackageContext(pluginPackage);
  361. if (context == null) {
  362. return;
  363. }
  364. if (targetPackages.length == 0) {
  365. Log.w(LOG_TAG, "Plug-in does not define \"packages\" meta-data mapping: "
  366. + pluginPackage + ". Ignoring!");
  367. return;
  368. }
  369. if (speechStrategies.length == 0) {
  370. Log.w(LOG_TAG, "Plug-in does not define \"speechstrategies\" meta-data mapping: "
  371. + pluginPackage + ". Ignoring!");
  372. return;
  373. }
  374. if (targetPackages.length < speechStrategies.length) {
  375. Log.w(LOG_TAG, "#packages < #speechstrategies => Loading only speech strategies"
  376. + " for the declared packages and ignoring the rest speech strategies.");
  377. } else if (targetPackages.length > speechStrategies.length) {
  378. Log.w(LOG_TAG, "#packages > #speechstrategies => Loading only packages for"
  379. + " the declared speech strategies and ignoring the rest packages.");
  380. }
  381. Resources resources = context.getResources();
  382. for (int i = 0, count = targetPackages.length; i < count; i++) {
  383. // check if more packages than speech strategies
  384. if (i >= speechStrategies.length) {
  385. return;
  386. }
  387. String speechStrategyResourceName = speechStrategies[i].replace(".xml", "");
  388. int resourceId = resources.getIdentifier(context.getPackageName() + ":raw/"
  389. + speechStrategyResourceName, null, null);
  390. mSpeechRuleProcessor.addSpeechStrategy(context, pluginInfo.publicSourceDir,
  391. targetPackages[i], resourceId);
  392. }
  393. }
  394. /**
  395. * @return {@link Context} instance for the given <code>packageName</code>.
  396. */
  397. private Context createPackageContext(String packageName) {
  398. try {
  399. int flags = (Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
  400. return mContext.getApplicationContext().createPackageContext(packageName, flags);
  401. } catch (NameNotFoundException nnfe) {
  402. Log.e(LOG_TAG, "Error creating package context.", nnfe);
  403. }
  404. return null;
  405. }
  406. /**
  407. * Worker for asynchronous plug-in loading.
  408. */
  409. class Worker implements Runnable {
  410. /**
  411. * Lock to ensure {@link Handler} instance.
  412. */
  413. final Object mWorkerLock = new Object();
  414. /**
  415. * {@link Handler} for asynchronous message processing.
  416. */
  417. Handler mHandler;
  418. Callback mCallback;
  419. /**
  420. * Creates a new worker whose handler's messages are handled
  421. * by the provided <code>callback</code>.
  422. */
  423. Worker(Handler.Callback callbalk) {
  424. mCallback = callbalk;
  425. }
  426. @Override
  427. public void run() {
  428. synchronized (mWorkerLock) {
  429. Looper.prepare();
  430. mHandler = new Handler(mCallback);
  431. mWorkerLock.notify();
  432. }
  433. Looper.loop();
  434. }
  435. /**
  436. * @return This worker's {@link Handler}.
  437. */
  438. public Handler getHandler() {
  439. return mHandler;
  440. }
  441. /**
  442. * Starts the worker.
  443. */
  444. public void start() {
  445. Thread thread = new Thread(this);
  446. thread.start();
  447. synchronized (mWorkerLock) {
  448. while (mHandler == null) {
  449. try {
  450. mWorkerLock.wait();
  451. } catch (InterruptedException ie) {
  452. /* ignore */
  453. }
  454. }
  455. }
  456. }
  457. /**
  458. * Stops the worker.
  459. */
  460. public void stop() {
  461. mHandler.getLooper().quit();
  462. }
  463. }
  464. /**
  465. * Helper class to hold together plug-in info.
  466. */
  467. class PluginInfo {
  468. boolean enabled;
  469. ServiceInfo serviceInfo;
  470. String publicSourceDir;
  471. String[] targetPackages;
  472. String[] speechStrategies;
  473. }
  474. /**
  475. * Handles the removal of a <code>packageName</code>.
  476. */
  477. private void handlePackageRemoved(String packageName) {
  478. synchronized (mLock) {
  479. Iterator<PluginInfo> iterator = mPluginCache.values()
  480. .iterator();
  481. while (iterator.hasNext()) {
  482. PluginInfo pluginInfo = iterator.next();
  483. if (!pluginInfo.serviceInfo.packageName.equals(packageName)) {
  484. continue;
  485. }
  486. iterator.remove();
  487. unloadPlugin(pluginInfo);
  488. }
  489. }
  490. }
  491. /**
  492. * Handles the addition or change of a <code>packageName</code>.
  493. */
  494. private void handlePackageAddedOrChanged(String packageName) {
  495. ServiceInfo plugin = getPluginForPackage(packageName);
  496. if (plugin != null) {
  497. if (!isValidPlugin(plugin)) {
  498. return;
  499. }
  500. String key = new ComponentName(plugin.packageName, plugin.name)
  501. .flattenToString();
  502. PluginInfo pluginInfo = createPluginInfo(plugin);
  503. synchronized (mLock) {
  504. mPluginCache.put(key, pluginInfo);
  505. if (pluginInfo.enabled) {
  506. loadPlugin(pluginInfo);
  507. }
  508. }
  509. }
  510. }
  511. /**
  512. * @return The plug-in for a <code>pacakgeName</code> or null if no such.
  513. */
  514. private ServiceInfo getPluginForPackage(String packageName) {
  515. List<ResolveInfo> resolveInfos = mPackageManager.queryIntentServices(sPluginIntent,
  516. PackageManager.GET_META_DATA);
  517. for (ResolveInfo resolveInfo : resolveInfos) {
  518. ServiceInfo serviceInfo = resolveInfo.serviceInfo;
  519. if (serviceInfo.packageName.equals(packageName)) {
  520. return serviceInfo;
  521. }
  522. }
  523. return null;
  524. }
  525. }