PageRenderTime 51ms CodeModel.GetById 13ms app.highlight 32ms RepoModel.GetById 1ms app.codeStats 0ms

/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
 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}