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