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
17package android.webkit;
18
19import java.util.ArrayList;
20import java.util.List;
21
22import android.annotation.SdkConstant;
23import android.annotation.SdkConstant.SdkConstantType;
24import android.content.Context;
25import android.content.Intent;
26import android.content.pm.ApplicationInfo;
27import android.content.pm.PackageInfo;
28import android.content.pm.PackageManager;
29import android.content.pm.ResolveInfo;
30import android.content.pm.ServiceInfo;
31import android.content.pm.Signature;
32import android.content.pm.PackageManager.NameNotFoundException;
33import android.os.SystemProperties;
34import android.util.Log;
35
36/**
37 * Class for managing the relationship between the {@link WebView} and installed
38 * plugins in the system. You can find this class through
39 * {@link PluginManager#getInstance}.
40 *
41 * @hide pending API solidification
42 */
43public class PluginManager {
44
45    /**
46     * Service Action: A plugin wishes to be loaded in the WebView must provide
47     * {@link android.content.IntentFilter IntentFilter} that accepts this
48     * action in their AndroidManifest.xml.
49     * <p>
50     * TODO: we may change this to a new PLUGIN_ACTION if this is going to be
51     * public.
52     */
53    @SdkConstant(SdkConstantType.SERVICE_ACTION)
54    public static final String PLUGIN_ACTION = "android.webkit.PLUGIN";
55
56    /**
57     * A plugin wishes to be loaded in the WebView must provide this permission
58     * in their AndroidManifest.xml.
59     */
60    public static final String PLUGIN_PERMISSION = "android.webkit.permission.PLUGIN";
61
62    private static final String LOGTAG = "PluginManager";
63
64    private static final String PLUGIN_SYSTEM_LIB = "/system/lib/plugins/";
65
66    private static final String PLUGIN_TYPE = "type";
67    private static final String TYPE_NATIVE = "native";
68
69    private static PluginManager mInstance = null;
70
71    private final Context mContext;
72
73    private ArrayList<PackageInfo> mPackageInfoCache;
74
75    // Only plugin matches one of the signatures in the list can be loaded
76    // inside the WebView process
77    private static final String SIGNATURE_1 = "308204c5308203ada003020102020900d7cb412f75f4887e300d06092a864886f70d010105050030819d310b3009060355040613025553311330110603550408130a43616c69666f726e69613111300f0603550407130853616e204a6f736531233021060355040a131a41646f62652053797374656d7320496e636f72706f7261746564311c301a060355040b1313496e666f726d6174696f6e2053797374656d73312330210603550403131a41646f62652053797374656d7320496e636f72706f7261746564301e170d3039313030313030323331345a170d3337303231363030323331345a30819d310b3009060355040613025553311330110603550408130a43616c69666f726e69613111300f0603550407130853616e204a6f736531233021060355040a131a41646f62652053797374656d7320496e636f72706f7261746564311c301a060355040b1313496e666f726d6174696f6e2053797374656d73312330210603550403131a41646f62652053797374656d7320496e636f72706f726174656430820120300d06092a864886f70d01010105000382010d0030820108028201010099724f3e05bbd78843794f357776e04b340e13cb1c9ccb3044865180d7d8fec8166c5bbd876da8b80aa71eb6ba3d4d3455c9a8de162d24a25c4c1cd04c9523affd06a279fc8f0d018f242486bdbb2dbfbf6fcb21ed567879091928b876f7ccebc7bccef157366ebe74e33ae1d7e9373091adab8327482154afc0693a549522f8c796dd84d16e24bb221f5dbb809ca56dd2b6e799c5fa06b6d9c5c09ada54ea4c5db1523a9794ed22a3889e5e05b29f8ee0a8d61efe07ae28f65dece2ff7edc5b1416d7c7aad7f0d35e8f4a4b964dbf50ae9aa6d620157770d974131b3e7e3abd6d163d65758e2f0822db9c88598b9db6263d963d13942c91fc5efe34fc1e06e3020103a382010630820102301d0603551d0e041604145af418e419a639e1657db960996364a37ef20d403081d20603551d230481ca3081c780145af418e419a639e1657db960996364a37ef20d40a181a3a481a030819d310b3009060355040613025553311330110603550408130a43616c69666f726e69613111300f0603550407130853616e204a6f736531233021060355040a131a41646f62652053797374656d7320496e636f72706f7261746564311c301a060355040b1313496e666f726d6174696f6e2053797374656d73312330210603550403131a41646f62652053797374656d7320496e636f72706f7261746564820900d7cb412f75f4887e300c0603551d13040530030101ff300d06092a864886f70d0101050500038201010076c2a11fe303359689c2ebc7b2c398eff8c3f9ad545cdbac75df63bf7b5395b6988d1842d6aa1556d595b5692e08224d667a4c9c438f05e74906c53dd8016dde7004068866f01846365efd146e9bfaa48c9ecf657f87b97c757da11f225c4a24177bf2d7188e6cce2a70a1e8a841a14471eb51457398b8a0addd8b6c8c1538ca8f1e40b4d8b960009ea22c188d28924813d2c0b4a4d334b7cf05507e1fcf0a06fe946c7ffc435e173af6fc3e3400643710acc806f830a14788291d46f2feed9fb5c70423ca747ed1572d752894ac1f19f93989766308579393fabb43649aa8806a313b1ab9a50922a44c2467b9062037f2da0d484d9ffd8fe628eeea629ba637";
78
79    private static final Signature[] SIGNATURES = new Signature[] {
80        new Signature(SIGNATURE_1)
81    };
82
83    private PluginManager(Context context) {
84        mContext = context;
85        mPackageInfoCache = new ArrayList<PackageInfo>();
86    }
87
88    public static synchronized PluginManager getInstance(Context context) {
89        if (mInstance == null) {
90            if (context == null) {
91                throw new IllegalStateException(
92                        "First call to PluginManager need a valid context.");
93            }
94            mInstance = new PluginManager(context.getApplicationContext());
95        }
96        return mInstance;
97    }
98
99    /**
100     * Signal the WebCore thread to refresh its list of plugins. Use this if the
101     * directory contents of one of the plugin directories has been modified and
102     * needs its changes reflecting. May cause plugin load and/or unload.
103     *
104     * @param reloadOpenPages Set to true to reload all open pages.
105     */
106    public void refreshPlugins(boolean reloadOpenPages) {
107        BrowserFrame.sJavaBridge.obtainMessage(
108                JWebCoreJavaBridge.REFRESH_PLUGINS, reloadOpenPages)
109                .sendToTarget();
110    }
111
112    String[] getPluginDirectories() {
113
114        ArrayList<String> directories = new ArrayList<String>();
115        PackageManager pm = mContext.getPackageManager();
116        List<ResolveInfo> plugins = pm.queryIntentServices(new Intent(PLUGIN_ACTION),
117                PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
118
119        synchronized(mPackageInfoCache) {
120
121            // clear the list of existing packageInfo objects
122            mPackageInfoCache.clear();
123
124            for (ResolveInfo info : plugins) {
125
126                // retrieve the plugin's service information
127                ServiceInfo serviceInfo = info.serviceInfo;
128                if (serviceInfo == null) {
129                    Log.w(LOGTAG, "Ignore bad plugin");
130                    continue;
131                }
132
133                // retrieve information from the plugin's manifest
134                PackageInfo pkgInfo;
135                try {
136                    pkgInfo = pm.getPackageInfo(serviceInfo.packageName,
137                                    PackageManager.GET_PERMISSIONS
138                                    | PackageManager.GET_SIGNATURES);
139                } catch (NameNotFoundException e) {
140                    Log.w(LOGTAG, "Can't find plugin: " + serviceInfo.packageName);
141                    continue;
142                }
143                if (pkgInfo == null) {
144                    continue;
145                }
146
147                /*
148                 * find the location of the plugin's shared library. The default
149                 * is to assume the app is either a user installed app or an
150                 * updated system app. In both of these cases the library is
151                 * stored in the app's data directory.
152                 */
153                String directory = pkgInfo.applicationInfo.dataDir + "/lib";
154                final int appFlags = pkgInfo.applicationInfo.flags;
155                final int updatedSystemFlags = ApplicationInfo.FLAG_SYSTEM |
156                                               ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
157                // preloaded system app with no user updates
158                if ((appFlags & updatedSystemFlags) == ApplicationInfo.FLAG_SYSTEM) {
159                    directory = PLUGIN_SYSTEM_LIB + pkgInfo.packageName;
160                }
161
162                // check if the plugin has the required permissions and
163                // signatures
164                if (!containsPluginPermissionAndSignatures(pkgInfo)) {
165                    continue;
166                }
167
168                // determine the type of plugin from the manifest
169                if (serviceInfo.metaData == null) {
170                    Log.e(LOGTAG, "The plugin '" + serviceInfo.name + "' has no type defined");
171                    continue;
172                }
173
174                String pluginType = serviceInfo.metaData.getString(PLUGIN_TYPE);
175                if (!TYPE_NATIVE.equals(pluginType)) {
176                    Log.e(LOGTAG, "Unrecognized plugin type: " + pluginType);
177                    continue;
178                }
179
180                try {
181                    Class<?> cls = getPluginClass(serviceInfo.packageName, serviceInfo.name);
182
183                    //TODO implement any requirements of the plugin class here!
184                    boolean classFound = true;
185
186                    if (!classFound) {
187                        Log.e(LOGTAG, "The plugin's class' " + serviceInfo.name + "' does not extend the appropriate class.");
188                        continue;
189                    }
190
191                } catch (NameNotFoundException e) {
192                    Log.e(LOGTAG, "Can't find plugin: " + serviceInfo.packageName);
193                    continue;
194                } catch (ClassNotFoundException e) {
195                    Log.e(LOGTAG, "Can't find plugin's class: " + serviceInfo.name);
196                    continue;
197                }
198
199                // if all checks have passed then make the plugin available
200                mPackageInfoCache.add(pkgInfo);
201                directories.add(directory);
202            }
203        }
204
205        return directories.toArray(new String[directories.size()]);
206    }
207
208    /* package */
209    boolean containsPluginPermissionAndSignatures(String pluginAPKName) {
210        PackageManager pm = mContext.getPackageManager();
211
212        // retrieve information from the plugin's manifest
213        try {
214            PackageInfo pkgInfo = pm.getPackageInfo(pluginAPKName, PackageManager.GET_PERMISSIONS
215                    | PackageManager.GET_SIGNATURES);
216            if (pkgInfo != null) {
217                return containsPluginPermissionAndSignatures(pkgInfo);
218            }
219        } catch (NameNotFoundException e) {
220            Log.w(LOGTAG, "Can't find plugin: " + pluginAPKName);
221        }
222        return false;
223    }
224
225    private static boolean containsPluginPermissionAndSignatures(PackageInfo pkgInfo) {
226
227        // check if the plugin has the required permissions
228        String permissions[] = pkgInfo.requestedPermissions;
229        if (permissions == null) {
230            return false;
231        }
232        boolean permissionOk = false;
233        for (String permit : permissions) {
234            if (PLUGIN_PERMISSION.equals(permit)) {
235                permissionOk = true;
236                break;
237            }
238        }
239        if (!permissionOk) {
240            return false;
241        }
242
243        // check to ensure the plugin is properly signed
244        Signature signatures[] = pkgInfo.signatures;
245        if (signatures == null) {
246            return false;
247        }
248        if (SystemProperties.getBoolean("ro.secure", false)) {
249            boolean signatureMatch = false;
250            for (Signature signature : signatures) {
251                for (int i = 0; i < SIGNATURES.length; i++) {
252                    if (SIGNATURES[i].equals(signature)) {
253                        signatureMatch = true;
254                        break;
255                    }
256                }
257            }
258            if (!signatureMatch) {
259                return false;
260            }
261        }
262
263        return true;
264    }
265
266    /* package */
267    String getPluginsAPKName(String pluginLib) {
268
269        // basic error checking on input params
270        if (pluginLib == null || pluginLib.length() == 0) {
271            return null;
272        }
273
274        // must be synchronized to ensure the consistency of the cache
275        synchronized(mPackageInfoCache) {
276            for (PackageInfo pkgInfo : mPackageInfoCache) {
277                if (pluginLib.contains(pkgInfo.packageName)) {
278                    return pkgInfo.packageName;
279                }
280            }
281        }
282
283        // if no apk was found then return null
284        return null;
285    }
286
287    String getPluginSharedDataDirectory() {
288        return mContext.getDir("plugins", 0).getPath();
289    }
290
291    /* package */
292    Class<?> getPluginClass(String packageName, String className)
293            throws NameNotFoundException, ClassNotFoundException {
294        Context pluginContext = mContext.createPackageContext(packageName,
295                Context.CONTEXT_INCLUDE_CODE |
296                Context.CONTEXT_IGNORE_SECURITY);
297        ClassLoader pluginCL = pluginContext.getClassLoader();
298        return pluginCL.loadClass(className);
299    }
300}
301