chromium/android_webview/glue/java/src/com/android/webview/chromium/SplitApkWorkaround.java

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package com.android.webview.chromium;

import android.annotation.SuppressLint;

import dalvik.system.BaseDexClassLoader;

import org.chromium.base.Log;

import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Map;

/**
 * WebView-side workaround for the Android O framework bug described in https://crbug.com/889954
 * which affects us if we are the current WebView provider and we were installed as a split APK.
 */
public class SplitApkWorkaround {
    private static final String TAG = "SplitApkWorkaround";

    /**
     * There is a framework bug in O that causes an incorrect classloader cache entry to be created
     * when the WebView provider is installed as multiple split APKs.
     * Use reflection to correct the cache entry during WebView zygote startup.
     * This function runs in the WebView zygote, which cannot make any binder calls to the framework
     * and is a very restricted environment.
     */
    @SuppressLint("DiscouragedPrivateApi")
    @SuppressWarnings("unchecked")
    public static void apply() {
        try {
            // Retrieve all the required classes and fields first, such that if any of the lookups
            // fail, we won't have done anything yet.
            Class<?> alClass = Class.forName("android.app.ApplicationLoaders");
            Method getDefaultMethod = alClass.getDeclaredMethod("getDefault");
            Field mLoadersField = alClass.getDeclaredField("mLoaders");
            mLoadersField.setAccessible(true);
            Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
            pathListField.setAccessible(true);
            Class<?> dplClass = Class.forName("dalvik.system.DexPathList");
            Field dexElementsField = dplClass.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);
            Class<?> elClass = Class.forName("dalvik.system.DexPathList$Element");
            Field pathField = elClass.getDeclaredField("path");
            pathField.setAccessible(true);

            // Retrieve the ApplicationLoaders singleton and get the cache from inside it.
            Object alInstance = getDefaultMethod.invoke(null);
            Object rawLoaders = mLoadersField.get(alInstance);
            Map<String, ClassLoader> loaders = (Map<String, ClassLoader>) rawLoaders;

            // Synchronize on the map while trying to update it, as the framework does.
            synchronized (loaders) {
                // Copy of loaders keys, since we plan to modify loaders while iterating.
                ArrayList<String> keys = new ArrayList(loaders.keySet());
                for (String cacheKey : keys) {
                    try {
                        ClassLoader value = loaders.get(cacheKey);
                        if (!(value instanceof BaseDexClassLoader)) {
                            // If it's some other type it can't be the right one.
                            continue;
                        }
                        BaseDexClassLoader cl = (BaseDexClassLoader) value;

                        // Get the list of files that this classloader uses as its classpath.
                        Object pathList = pathListField.get(cl);
                        Object dexElements = dexElementsField.get(pathList);

                        int elementCount = Array.getLength(dexElements);
                        if (elementCount <= 1) {
                            // If there's only one file, then this classloader cannot be affected by
                            // the bug, so ignore it.
                            continue;
                        }

                        // If there's more than one file, get the first file in the path.
                        // If it's the same as our cache key, then the cache key must, by
                        // definition, be incomplete - listing only one file when it should list
                        // all of them.
                        Object firstElement = Array.get(dexElements, 0);
                        File firstPath = (File) pathField.get(firstElement);
                        if (cacheKey.equals(firstPath.getPath())) {
                            // Build a new, correct cache key by concatenating all the files in the
                            // list together in order, separated by colons.
                            String newCacheKey = cacheKey;
                            for (int i = 1; i < elementCount; i++) {
                                Object element = Array.get(dexElements, i);
                                File path = (File) pathField.get(element);
                                newCacheKey += ":" + path.getPath();
                            }

                            // Add a new entry to the cache which maps the new, correct key to
                            // the same classloader object. We do not remove the previous entry
                            // from the cache, in case something attempts to look it up by the
                            // old key for some reason - it shouldn't cause a problem for there
                            // to be multiple entries mapping to the same classloader.
                            loaders.put(newCacheKey, cl);
                            Log.i(TAG, "Fixed classloader cache entry for " + newCacheKey);
                        }
                    } catch (Exception e) {
                        // We log and ignore it here so we can continue looping through the cache,
                        // in the hope that the one that threw an exception wasn't the one we
                        // were looking for.
                        Log.w(TAG, "Caught exception while attempting to fix classloader cache", e);
                    }
                }
            }
        } catch (Exception e) {
            // If we got an exception at this point we assume that we failed to fix it, since we
            // didn't get as far as iterating over the cache entries.
            Log.w(TAG, "Caught exception while attempting to fix classloader cache", e);
            throw new RuntimeException(e);
        }
    }
}