WebViewFactory.java revision b98082dcfb45a82d9b12dbf0d2f88acffe96a1c4
1/*
2 * Copyright (C) 2012 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 android.annotation.SystemApi;
20import android.app.ActivityManagerInternal;
21import android.app.AppGlobals;
22import android.app.Application;
23import android.content.Context;
24import android.content.pm.ApplicationInfo;
25import android.content.pm.PackageInfo;
26import android.content.pm.PackageManager;
27import android.os.Build;
28import android.os.Process;
29import android.os.RemoteException;
30import android.os.ServiceManager;
31import android.os.StrictMode;
32import android.os.SystemProperties;
33import android.os.Trace;
34import android.text.TextUtils;
35import android.util.AndroidRuntimeException;
36import android.util.Log;
37
38import com.android.server.LocalServices;
39
40import dalvik.system.VMRuntime;
41
42import java.io.File;
43import java.io.IOException;
44import java.util.Arrays;
45import java.util.zip.ZipEntry;
46import java.util.zip.ZipFile;
47
48/**
49 * Top level factory, used creating all the main WebView implementation classes.
50 *
51 * @hide
52 */
53@SystemApi
54public final class WebViewFactory {
55
56    private static final String CHROMIUM_WEBVIEW_FACTORY =
57            "com.android.webview.chromium.WebViewChromiumFactoryProvider";
58
59    private static final String NULL_WEBVIEW_FACTORY =
60            "com.android.webview.nullwebview.NullWebViewFactoryProvider";
61
62    private static final String CHROMIUM_WEBVIEW_NATIVE_RELRO_32 =
63            "/data/misc/shared_relro/libwebviewchromium32.relro";
64    private static final String CHROMIUM_WEBVIEW_NATIVE_RELRO_64 =
65            "/data/misc/shared_relro/libwebviewchromium64.relro";
66
67    public static final String CHROMIUM_WEBVIEW_VMSIZE_SIZE_PROPERTY =
68            "persist.sys.webview.vmsize";
69    private static final long CHROMIUM_WEBVIEW_DEFAULT_VMSIZE_BYTES = 100 * 1024 * 1024;
70
71    private static final String LOGTAG = "WebViewFactory";
72
73    private static final boolean DEBUG = false;
74
75    // Cache the factory both for efficiency, and ensure any one process gets all webviews from the
76    // same provider.
77    private static WebViewFactoryProvider sProviderInstance;
78    private static final Object sProviderLock = new Object();
79    private static boolean sAddressSpaceReserved = false;
80    private static PackageInfo sPackageInfo;
81
82    // Error codes for loadWebViewNativeLibraryFromPackage
83    public static final int LIBLOAD_SUCCESS = 0;
84    public static final int LIBLOAD_WRONG_PACKAGE_NAME = 1;
85    public static final int LIBLOAD_ADDRESS_SPACE_NOT_RESERVED = 2;
86    public static final int LIBLOAD_FAILED_WAITING_FOR_RELRO = 3;
87    public static final int LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES = 4;
88
89    // native relro loading error codes
90    public static final int LIBLOAD_FAILED_TO_OPEN_RELRO_FILE = 5;
91    public static final int LIBLOAD_FAILED_TO_LOAD_LIBRARY = 6;
92    public static final int LIBLOAD_FAILED_JNI_CALL = 7;
93
94    private static class MissingWebViewPackageException extends AndroidRuntimeException {
95        public MissingWebViewPackageException(String message) { super(message); }
96        public MissingWebViewPackageException(Exception e) { super(e); }
97    }
98
99    /** @hide */
100    public static String[] getWebViewPackageNames() {
101        return AppGlobals.getInitialApplication().getResources().getStringArray(
102                com.android.internal.R.array.config_webViewPackageNames);
103    }
104
105    // TODO (gsennton) remove when committing webview xts test change
106    public static String getWebViewPackageName() {
107        String[] webViewPackageNames = getWebViewPackageNames();
108        return webViewPackageNames[webViewPackageNames.length-1];
109    }
110
111    /**
112     * Return the package info of the first package in the webview priority list that contains
113     * webview.
114     *
115     * @hide
116     */
117    public static PackageInfo findPreferredWebViewPackage() {
118        PackageManager pm = AppGlobals.getInitialApplication().getPackageManager();
119
120        for (String packageName : getWebViewPackageNames()) {
121            try {
122                PackageInfo packageInfo = pm.getPackageInfo(packageName,
123                    PackageManager.GET_META_DATA);
124                ApplicationInfo applicationInfo = packageInfo.applicationInfo;
125
126                // If the correct flag is set the package contains webview.
127                if (getWebViewLibrary(applicationInfo) != null) {
128                    return packageInfo;
129                }
130            } catch (PackageManager.NameNotFoundException e) {
131            }
132        }
133        throw new MissingWebViewPackageException("Could not find a loadable WebView package");
134    }
135
136    // throws MissingWebViewPackageException
137    private static ApplicationInfo getWebViewApplicationInfo() {
138        if (sPackageInfo == null)
139            return findPreferredWebViewPackage().applicationInfo;
140        else
141            return sPackageInfo.applicationInfo;
142    }
143
144    private static String getWebViewLibrary(ApplicationInfo ai) {
145        if (ai.metaData != null)
146            return ai.metaData.getString("com.android.webview.WebViewLibrary");
147        return null;
148    }
149
150    public static PackageInfo getLoadedPackageInfo() {
151        return sPackageInfo;
152    }
153
154    /**
155     * Load the native library for the given package name iff that package
156     * name is the same as the one providing the current webview.
157     */
158    public static int loadWebViewNativeLibraryFromPackage(String packageName) {
159        sPackageInfo = findPreferredWebViewPackage();
160        if (packageName != null && packageName.equals(sPackageInfo.packageName)) {
161            return loadNativeLibrary();
162        }
163        return LIBLOAD_WRONG_PACKAGE_NAME;
164    }
165
166    static WebViewFactoryProvider getProvider() {
167        synchronized (sProviderLock) {
168            // For now the main purpose of this function (and the factory abstraction) is to keep
169            // us honest and minimize usage of WebView internals when binding the proxy.
170            if (sProviderInstance != null) return sProviderInstance;
171
172            final int uid = android.os.Process.myUid();
173            if (uid == android.os.Process.ROOT_UID || uid == android.os.Process.SYSTEM_UID) {
174                throw new UnsupportedOperationException(
175                        "For security reasons, WebView is not allowed in privileged processes");
176            }
177
178            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getProvider()");
179            try {
180                Class<WebViewFactoryProvider> providerClass = getProviderClass();
181
182                StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
183                Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "providerClass.newInstance()");
184                try {
185                    sProviderInstance = providerClass.getConstructor(WebViewDelegate.class)
186                            .newInstance(new WebViewDelegate());
187                    if (DEBUG) Log.v(LOGTAG, "Loaded provider: " + sProviderInstance);
188                    return sProviderInstance;
189                } catch (Exception e) {
190                    Log.e(LOGTAG, "error instantiating provider", e);
191                    throw new AndroidRuntimeException(e);
192                } finally {
193                    Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
194                    StrictMode.setThreadPolicy(oldPolicy);
195                }
196            } finally {
197                Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
198            }
199        }
200    }
201
202    private static Class<WebViewFactoryProvider> getProviderClass() {
203        try {
204            // First fetch the package info so we can log the webview package version.
205            sPackageInfo = findPreferredWebViewPackage();
206            Log.i(LOGTAG, "Loading " + sPackageInfo.packageName + " version " +
207                sPackageInfo.versionName + " (code " + sPackageInfo.versionCode + ")");
208
209            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.loadNativeLibrary()");
210            loadNativeLibrary();
211            Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
212
213            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getChromiumProviderClass()");
214            try {
215                return getChromiumProviderClass();
216            } catch (ClassNotFoundException e) {
217                Log.e(LOGTAG, "error loading provider", e);
218                throw new AndroidRuntimeException(e);
219            } finally {
220                Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
221            }
222        } catch (MissingWebViewPackageException e) {
223            // If the package doesn't exist, then try loading the null WebView instead.
224            // If that succeeds, then this is a device without WebView support; if it fails then
225            // swallow the failure, complain that the real WebView is missing and rethrow the
226            // original exception.
227            try {
228                return (Class<WebViewFactoryProvider>) Class.forName(NULL_WEBVIEW_FACTORY);
229            } catch (ClassNotFoundException e2) {
230                // Ignore.
231            }
232            Log.e(LOGTAG, "Chromium WebView package does not exist", e);
233            throw new AndroidRuntimeException(e);
234        }
235    }
236
237    // throws MissingWebViewPackageException
238    private static Class<WebViewFactoryProvider> getChromiumProviderClass()
239            throws ClassNotFoundException {
240        Application initialApplication = AppGlobals.getInitialApplication();
241        try {
242            // Construct a package context to load the Java code into the current app.
243            Context webViewContext = initialApplication.createPackageContext(
244                sPackageInfo.packageName,
245                Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
246            initialApplication.getAssets().addAssetPath(
247                    webViewContext.getApplicationInfo().sourceDir);
248            ClassLoader clazzLoader = webViewContext.getClassLoader();
249            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "Class.forName()");
250            try {
251                return (Class<WebViewFactoryProvider>) Class.forName(CHROMIUM_WEBVIEW_FACTORY, true,
252                                                                     clazzLoader);
253            } finally {
254                Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
255            }
256        } catch (PackageManager.NameNotFoundException e) {
257            throw new MissingWebViewPackageException(e);
258        }
259    }
260
261    /**
262     * Perform any WebView loading preparations that must happen in the zygote.
263     * Currently, this means allocating address space to load the real JNI library later.
264     */
265    public static void prepareWebViewInZygote() {
266        try {
267            System.loadLibrary("webviewchromium_loader");
268            long addressSpaceToReserve =
269                    SystemProperties.getLong(CHROMIUM_WEBVIEW_VMSIZE_SIZE_PROPERTY,
270                    CHROMIUM_WEBVIEW_DEFAULT_VMSIZE_BYTES);
271            sAddressSpaceReserved = nativeReserveAddressSpace(addressSpaceToReserve);
272
273            if (sAddressSpaceReserved) {
274                if (DEBUG) {
275                    Log.v(LOGTAG, "address space reserved: " + addressSpaceToReserve + " bytes");
276                }
277            } else {
278                Log.e(LOGTAG, "reserving " + addressSpaceToReserve +
279                        " bytes of address space failed");
280            }
281        } catch (Throwable t) {
282            // Log and discard errors at this stage as we must not crash the zygote.
283            Log.e(LOGTAG, "error preparing native loader", t);
284        }
285    }
286
287    /**
288     * Perform any WebView loading preparations that must happen at boot from the system server,
289     * after the package manager has started or after an update to the webview is installed.
290     * This must be called in the system server.
291     * Currently, this means spawning the child processes which will create the relro files.
292     */
293    public static void prepareWebViewInSystemServer() {
294        String[] nativePaths = null;
295        try {
296            nativePaths = getWebViewNativeLibraryPaths();
297        } catch (Throwable t) {
298            // Log and discard errors at this stage as we must not crash the system server.
299            Log.e(LOGTAG, "error preparing webview native library", t);
300        }
301        prepareWebViewInSystemServer(nativePaths);
302    }
303
304    private static void prepareWebViewInSystemServer(String[] nativeLibraryPaths) {
305        if (DEBUG) Log.v(LOGTAG, "creating relro files");
306
307        // We must always trigger createRelRo regardless of the value of nativeLibraryPaths. Any
308        // unexpected values will be handled there to ensure that we trigger notifying any process
309        // waiting on relreo creation.
310        if (Build.SUPPORTED_32_BIT_ABIS.length > 0) {
311            if (DEBUG) Log.v(LOGTAG, "Create 32 bit relro");
312            createRelroFile(false /* is64Bit */, nativeLibraryPaths);
313        }
314
315        if (Build.SUPPORTED_64_BIT_ABIS.length > 0) {
316            if (DEBUG) Log.v(LOGTAG, "Create 64 bit relro");
317            createRelroFile(true /* is64Bit */, nativeLibraryPaths);
318        }
319    }
320
321    public static void onWebViewUpdateInstalled() {
322        String[] nativeLibs = null;
323        try {
324            nativeLibs = WebViewFactory.getWebViewNativeLibraryPaths();
325            if (nativeLibs != null) {
326                long newVmSize = 0L;
327
328                for (String path : nativeLibs) {
329                    if (path == null || TextUtils.isEmpty(path)) continue;
330                    if (DEBUG) Log.d(LOGTAG, "Checking file size of " + path);
331                    File f = new File(path);
332                    if (f.exists()) {
333                        newVmSize = Math.max(newVmSize, f.length());
334                        continue;
335                    }
336                    if (path.contains("!")) {
337                        String[] split = TextUtils.split(path, "!");
338                        if (split.length == 2) {
339                            try {
340                                ZipFile z = new ZipFile(split[0]);
341                                ZipEntry e = z.getEntry(split[1]);
342                                if (e != null && e.getMethod() == ZipEntry.STORED) {
343                                    newVmSize = Math.max(newVmSize, e.getSize());
344                                    continue;
345                                }
346                            }
347                            catch (IOException e) {
348                                Log.e(LOGTAG, "error reading APK file " + split[0] + ", ", e);
349                            }
350                        }
351                    }
352                    Log.e(LOGTAG, "error sizing load for " + path);
353                }
354
355                if (DEBUG) {
356                    Log.v(LOGTAG, "Based on library size, need " + newVmSize +
357                            " bytes of address space.");
358                }
359                // The required memory can be larger than the file on disk (due to .bss), and an
360                // upgraded version of the library will likely be larger, so always attempt to
361                // reserve twice as much as we think to allow for the library to grow during this
362                // boot cycle.
363                newVmSize = Math.max(2 * newVmSize, CHROMIUM_WEBVIEW_DEFAULT_VMSIZE_BYTES);
364                Log.d(LOGTAG, "Setting new address space to " + newVmSize);
365                SystemProperties.set(CHROMIUM_WEBVIEW_VMSIZE_SIZE_PROPERTY,
366                        Long.toString(newVmSize));
367            }
368        } catch (Throwable t) {
369            // Log and discard errors at this stage as we must not crash the system server.
370            Log.e(LOGTAG, "error preparing webview native library", t);
371        }
372        prepareWebViewInSystemServer(nativeLibs);
373    }
374
375    // throws MissingWebViewPackageException
376    private static String getLoadFromApkPath(String apkPath,
377                                             String[] abiList,
378                                             String nativeLibFileName) {
379        // Search the APK for a native library conforming to a listed ABI.
380        try {
381            ZipFile z = new ZipFile(apkPath);
382            for (String abi : abiList) {
383                final String entry = "lib/" + abi + "/" + nativeLibFileName;
384                ZipEntry e = z.getEntry(entry);
385                if (e != null && e.getMethod() == ZipEntry.STORED) {
386                    // Return a path formatted for dlopen() load from APK.
387                    return apkPath + "!" + entry;
388                }
389            }
390        } catch (IOException e) {
391            throw new MissingWebViewPackageException(e);
392        }
393        return "";
394    }
395
396    // throws MissingWebViewPackageException
397    private static String[] getWebViewNativeLibraryPaths() {
398        ApplicationInfo ai = getWebViewApplicationInfo();
399        final String NATIVE_LIB_FILE_NAME = getWebViewLibrary(ai);
400
401        String path32;
402        String path64;
403        boolean primaryArchIs64bit = VMRuntime.is64BitAbi(ai.primaryCpuAbi);
404        if (!TextUtils.isEmpty(ai.secondaryCpuAbi)) {
405            // Multi-arch case.
406            if (primaryArchIs64bit) {
407                // Primary arch: 64-bit, secondary: 32-bit.
408                path64 = ai.nativeLibraryDir;
409                path32 = ai.secondaryNativeLibraryDir;
410            } else {
411                // Primary arch: 32-bit, secondary: 64-bit.
412                path64 = ai.secondaryNativeLibraryDir;
413                path32 = ai.nativeLibraryDir;
414            }
415        } else if (primaryArchIs64bit) {
416            // Single-arch 64-bit.
417            path64 = ai.nativeLibraryDir;
418            path32 = "";
419        } else {
420            // Single-arch 32-bit.
421            path32 = ai.nativeLibraryDir;
422            path64 = "";
423        }
424
425        // Form the full paths to the extracted native libraries.
426        // If libraries were not extracted, try load from APK paths instead.
427        if (!TextUtils.isEmpty(path32)) {
428            path32 += "/" + NATIVE_LIB_FILE_NAME;
429            File f = new File(path32);
430            if (!f.exists()) {
431                path32 = getLoadFromApkPath(ai.sourceDir,
432                                            Build.SUPPORTED_32_BIT_ABIS,
433                                            NATIVE_LIB_FILE_NAME);
434            }
435        }
436        if (!TextUtils.isEmpty(path64)) {
437            path64 += "/" + NATIVE_LIB_FILE_NAME;
438            File f = new File(path64);
439            if (!f.exists()) {
440                path64 = getLoadFromApkPath(ai.sourceDir,
441                                            Build.SUPPORTED_64_BIT_ABIS,
442                                            NATIVE_LIB_FILE_NAME);
443            }
444        }
445
446        if (DEBUG) Log.v(LOGTAG, "Native 32-bit lib: " + path32 + ", 64-bit lib: " + path64);
447        return new String[] { path32, path64 };
448    }
449
450    private static void createRelroFile(final boolean is64Bit, String[] nativeLibraryPaths) {
451        final String abi =
452                is64Bit ? Build.SUPPORTED_64_BIT_ABIS[0] : Build.SUPPORTED_32_BIT_ABIS[0];
453
454        // crashHandler is invoked by the ActivityManagerService when the isolated process crashes.
455        Runnable crashHandler = new Runnable() {
456            @Override
457            public void run() {
458                try {
459                    Log.e(LOGTAG, "relro file creator for " + abi + " crashed. Proceeding without");
460                    getUpdateService().notifyRelroCreationCompleted(is64Bit, false);
461                } catch (RemoteException e) {
462                    Log.e(LOGTAG, "Cannot reach WebViewUpdateService. " + e.getMessage());
463                }
464            }
465        };
466
467        try {
468            if (nativeLibraryPaths == null
469                    || nativeLibraryPaths[0] == null || nativeLibraryPaths[1] == null) {
470                throw new IllegalArgumentException(
471                        "Native library paths to the WebView RelRo process must not be null!");
472            }
473            int pid = LocalServices.getService(ActivityManagerInternal.class).startIsolatedProcess(
474                    RelroFileCreator.class.getName(), nativeLibraryPaths, "WebViewLoader-" + abi, abi,
475                    Process.SHARED_RELRO_UID, crashHandler);
476            if (pid <= 0) throw new Exception("Failed to start the relro file creator process");
477        } catch (Throwable t) {
478            // Log and discard errors as we must not crash the system server.
479            Log.e(LOGTAG, "error starting relro file creator for abi " + abi, t);
480            crashHandler.run();
481        }
482    }
483
484    private static class RelroFileCreator {
485        // Called in an unprivileged child process to create the relro file.
486        public static void main(String[] args) {
487            boolean result = false;
488            boolean is64Bit = VMRuntime.getRuntime().is64Bit();
489            try{
490                if (args.length != 2 || args[0] == null || args[1] == null) {
491                    Log.e(LOGTAG, "Invalid RelroFileCreator args: " + Arrays.toString(args));
492                    return;
493                }
494                Log.v(LOGTAG, "RelroFileCreator (64bit = " + is64Bit + "), " +
495                        " 32-bit lib: " + args[0] + ", 64-bit lib: " + args[1]);
496                if (!sAddressSpaceReserved) {
497                    Log.e(LOGTAG, "can't create relro file; address space not reserved");
498                    return;
499                }
500                result = nativeCreateRelroFile(args[0] /* path32 */,
501                                               args[1] /* path64 */,
502                                               CHROMIUM_WEBVIEW_NATIVE_RELRO_32,
503                                               CHROMIUM_WEBVIEW_NATIVE_RELRO_64);
504                if (result && DEBUG) Log.v(LOGTAG, "created relro file");
505            } finally {
506                // We must do our best to always notify the update service, even if something fails.
507                try {
508                    getUpdateService().notifyRelroCreationCompleted(is64Bit, result);
509                } catch (RemoteException e) {
510                    Log.e(LOGTAG, "error notifying update service", e);
511                }
512
513                if (!result) Log.e(LOGTAG, "failed to create relro file");
514
515                // Must explicitly exit or else this process will just sit around after we return.
516                System.exit(0);
517            }
518        }
519    }
520
521    private static int loadNativeLibrary() {
522        if (!sAddressSpaceReserved) {
523            Log.e(LOGTAG, "can't load with relro file; address space not reserved");
524            return LIBLOAD_ADDRESS_SPACE_NOT_RESERVED;
525        }
526
527        try {
528            getUpdateService().waitForRelroCreationCompleted(VMRuntime.getRuntime().is64Bit());
529        } catch (RemoteException e) {
530            Log.e(LOGTAG, "error waiting for relro creation, proceeding without", e);
531            return LIBLOAD_FAILED_WAITING_FOR_RELRO;
532        }
533
534        try {
535            String[] args = getWebViewNativeLibraryPaths();
536            int result = nativeLoadWithRelroFile(args[0] /* path32 */,
537                                                     args[1] /* path64 */,
538                                                     CHROMIUM_WEBVIEW_NATIVE_RELRO_32,
539                                                     CHROMIUM_WEBVIEW_NATIVE_RELRO_64);
540            if (result != LIBLOAD_SUCCESS) {
541                Log.w(LOGTAG, "failed to load with relro file, proceeding without");
542            } else if (DEBUG) {
543                Log.v(LOGTAG, "loaded with relro file");
544            }
545            return result;
546        } catch (MissingWebViewPackageException e) {
547            Log.e(LOGTAG, "Failed to list WebView package libraries for loadNativeLibrary", e);
548            return LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES;
549        }
550    }
551
552    private static IWebViewUpdateService getUpdateService() {
553        return IWebViewUpdateService.Stub.asInterface(ServiceManager.getService("webviewupdate"));
554    }
555
556    private static native boolean nativeReserveAddressSpace(long addressSpaceToReserve);
557    private static native boolean nativeCreateRelroFile(String lib32, String lib64,
558                                                        String relro32, String relro64);
559    private static native int nativeLoadWithRelroFile(String lib32, String lib64,
560                                                          String relro32, String relro64);
561}
562