/*
 * Copyright (C) 2006 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.app;

import android.compat.annotation.UnsupportedAppUsage;
import android.content.pm.SharedLibraryInfo;
import android.os.Build;
import android.os.GraphicsEnvironment;
import android.os.Trace;
import android.util.ArrayMap;
import android.util.Log;

import com.android.internal.os.ClassLoaderFactory;

import dalvik.system.PathClassLoader;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/** @hide */
public class ApplicationLoaders {
    private static final String TAG = "ApplicationLoaders";

    @UnsupportedAppUsage
    public static ApplicationLoaders getDefault() {
        return gApplicationLoaders;
    }

    ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled,
                               String librarySearchPath, String libraryPermittedPath,
                               ClassLoader parent, String classLoaderName) {
        return getClassLoaderWithSharedLibraries(zip, targetSdkVersion, isBundled,
                              librarySearchPath, libraryPermittedPath, parent, classLoaderName,
                              null, null, null);
    }

    ClassLoader getClassLoaderWithSharedLibraries(
            String zip, int targetSdkVersion, boolean isBundled,
            String librarySearchPath, String libraryPermittedPath,
            ClassLoader parent, String classLoaderName,
            List<ClassLoader> sharedLibraries, List<String> nativeSharedLibraries,
            List<ClassLoader> sharedLibrariesLoadedAfterApp) {
        // For normal usage the cache key used is the same as the zip path.
        return getClassLoader(zip, targetSdkVersion, isBundled, librarySearchPath,
                              libraryPermittedPath, parent, zip, classLoaderName, sharedLibraries,
                              nativeSharedLibraries, sharedLibrariesLoadedAfterApp);
    }

    /**
     * Gets a class loader for a shared library. Additional dependent shared libraries are allowed
     * to be specified (sharedLibraries).
     *
     * Additionally, as an optimization, this will return a pre-created ClassLoader if one has
     * been cached by createAndCacheNonBootclasspathSystemClassLoaders.
     */
    ClassLoader getSharedLibraryClassLoaderWithSharedLibraries(String zip, int targetSdkVersion,
            boolean isBundled, String librarySearchPath, String libraryPermittedPath,
            ClassLoader parent, String classLoaderName, List<ClassLoader> sharedLibraries,
            List<ClassLoader> sharedLibrariesAfter) {
        ClassLoader loader = getCachedNonBootclasspathSystemLib(zip, parent, classLoaderName,
                sharedLibraries);
        if (loader != null) {
            return loader;
        }

        // TODO(b/142191088): allow (Java) shared libraries to have <uses-native-library>
        // Until that is supported, assume that all native shared libraries are used.
        // "ALL" is a magic string that libnativeloader uses to unconditionally add all available
        // native shared libraries to the classloader.
        List<String> nativeSharedLibraries = new ArrayList<>();
        nativeSharedLibraries.add("ALL");
        return getClassLoaderWithSharedLibraries(zip, targetSdkVersion, isBundled,
              librarySearchPath, libraryPermittedPath, parent, classLoaderName, sharedLibraries,
              nativeSharedLibraries, sharedLibrariesAfter);
    }

    private ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled,
                                       String librarySearchPath, String libraryPermittedPath,
                                       ClassLoader parent, String cacheKey,
                                       String classLoaderName, List<ClassLoader> sharedLibraries,
                                       List<String> nativeSharedLibraries,
                                       List<ClassLoader> sharedLibrariesLoadedAfterApp) {
        /*
         * This is the parent we use if they pass "null" in.  In theory
         * this should be the "system" class loader; in practice we
         * don't use that and can happily (and more efficiently) use the
         * bootstrap class loader.
         */
        ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();

        synchronized (mLoaders) {
            if (parent == null) {
                parent = baseParent;
            }

            /*
             * If we're one step up from the base class loader, find
             * something in our cache.  Otherwise, we create a whole
             * new ClassLoader for the zip archive.
             */
            if (parent == baseParent) {
                ClassLoader loader = mLoaders.get(cacheKey);
                if (loader != null) {
                    return loader;
                }

                Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);

                ClassLoader classloader = ClassLoaderFactory.createClassLoader(
                        zip,  librarySearchPath, libraryPermittedPath, parent,
                        targetSdkVersion, isBundled, classLoaderName, sharedLibraries,
                        nativeSharedLibraries, sharedLibrariesLoadedAfterApp);

                Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);

                Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "setLayerPaths");
                GraphicsEnvironment.getInstance().setLayerPaths(
                        classloader, librarySearchPath, libraryPermittedPath);
                Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);

                if (cacheKey != null) {
                    mLoaders.put(cacheKey, classloader);
                }
                return classloader;
            }

            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
            ClassLoader loader = ClassLoaderFactory.createClassLoader(
                    zip, null, parent, classLoaderName, sharedLibraries,
                    null /*sharedLibrariesLoadedAfterApp*/);
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
            return loader;
        }
    }

    /**
     * Caches system library class loaders which are not on the bootclasspath but are still used
     * by many system apps.
     *
     * All libraries in the closure of libraries to be loaded must be in libs. A library can
     * only depend on libraries that come before it in the list.
     */
    public void createAndCacheNonBootclasspathSystemClassLoaders(List<SharedLibraryInfo> libs) {
        if (mSystemLibsCacheMap != null) {
            throw new IllegalStateException("Already cached.");
        }

        mSystemLibsCacheMap = new HashMap<>();

        for (int i = 0; i < libs.size(); i++) {
            createAndCacheNonBootclasspathSystemClassLoader(libs.get(i));
        }
    }

    /**
     * Caches a single non-bootclasspath class loader.
     *
     * All of this library's dependencies must have previously been cached. Otherwise, an exception
     * is thrown.
     */
    private void createAndCacheNonBootclasspathSystemClassLoader(SharedLibraryInfo lib) {
        String path = lib.getPath();
        List<SharedLibraryInfo> dependencies = lib.getDependencies();

        // get cached classloaders for dependencies
        ArrayList<ClassLoader> sharedLibraries = null;
        if (dependencies != null) {
            sharedLibraries = new ArrayList<ClassLoader>(dependencies.size());
            for (SharedLibraryInfo dependency : dependencies) {
                String dependencyPath = dependency.getPath();
                CachedClassLoader cached = mSystemLibsCacheMap.get(dependencyPath);

                if (cached == null) {
                    throw new IllegalStateException("Failed to find dependency " + dependencyPath
                            + " of cachedlibrary " + path);
                }

                sharedLibraries.add(cached.loader);
            }
        }

        // assume cached libraries work with current sdk since they are built-in
        ClassLoader classLoader = getClassLoader(path, Build.VERSION.SDK_INT, true /*isBundled*/,
                null /*librarySearchPath*/, null /*libraryPermittedPath*/, null /*parent*/,
                null /*cacheKey*/, null /*classLoaderName*/, sharedLibraries /*sharedLibraries*/,
                null /* nativeSharedLibraries */, null /*sharedLibrariesLoadedAfterApp*/);

        if (classLoader == null) {
            // bad configuration or break in classloading code
            throw new IllegalStateException("Failed to cache " + path);
        }

        CachedClassLoader cached = new CachedClassLoader();
        cached.loader = classLoader;
        cached.sharedLibraries = sharedLibraries;

        Log.d(TAG, "Created zygote-cached class loader: " + path);
        mSystemLibsCacheMap.put(path, cached);
    }

    private static boolean sharedLibrariesEquals(List<ClassLoader> lhs, List<ClassLoader> rhs) {
        if (lhs == null) {
            return rhs == null;
        }

        return lhs.equals(rhs);
    }

    /**
     * Returns lib cached with createAndCacheNonBootclasspathSystemClassLoader. This is called by
     * the zygote during caching.
     *
     * If there is an error or the cache is not available, this returns null.
     */
    public ClassLoader getCachedNonBootclasspathSystemLib(String zip, ClassLoader parent,
            String classLoaderName, List<ClassLoader> sharedLibraries) {
        if (mSystemLibsCacheMap == null) {
            return null;
        }

        // we only cache top-level libs with the default class loader
        if (parent != null || classLoaderName != null) {
            return null;
        }

        CachedClassLoader cached = mSystemLibsCacheMap.get(zip);
        if (cached == null) {
            return null;
        }

        // cached must be built and loaded in the same environment
        if (!sharedLibrariesEquals(sharedLibraries, cached.sharedLibraries)) {
            Log.w(TAG, "Unexpected environment loading cached library " + zip + " (real|cached): ("
                    + sharedLibraries + "|" + cached.sharedLibraries + ")");
            return null;
        }

        Log.d(TAG, "Returning zygote-cached class loader: " + zip);
        return cached.loader;
    }

    /**
     * Creates a classloader for the WebView APK and places it in the cache of loaders maintained
     * by this class. This is used in the WebView zygote, where its presence in the cache speeds up
     * startup and enables memory sharing.
     */
    public ClassLoader createAndCacheWebViewClassLoader(String packagePath, String libsPath,
                                                        String cacheKey) {
        // The correct paths are calculated by WebViewZygote in the system server and passed to
        // us here. We hardcode the other parameters: WebView always targets the current SDK,
        // does not need to use non-public system libraries, and uses the base classloader as its
        // parent to permit usage of the cache.
        // The cache key is passed separately to enable the stub WebView to be cached under the
        // stub's APK path, when the actual package path is the donor APK.
        return getClassLoader(packagePath, Build.VERSION.SDK_INT, false, libsPath, null, null,
                              cacheKey, null /* classLoaderName */, null /* sharedLibraries */,
                              null /* nativeSharedLibraries */,
                              null /*sharedLibrariesLoadedAfterApp*/);
    }

    /**
     * Adds a new path the classpath of the given loader.
     * @throws IllegalStateException if the provided class loader is not a {@link PathClassLoader}.
     */
    void addPath(ClassLoader classLoader, String dexPath) {
        if (!(classLoader instanceof PathClassLoader)) {
            throw new IllegalStateException("class loader is not a PathClassLoader");
        }
        final PathClassLoader baseDexClassLoader = (PathClassLoader) classLoader;
        baseDexClassLoader.addDexPath(dexPath);
    }

    /**
     * @hide
     */
    void addNative(ClassLoader classLoader, Collection<String> libPaths) {
        if (!(classLoader instanceof PathClassLoader)) {
            throw new IllegalStateException("class loader is not a PathClassLoader");
        }
        final PathClassLoader baseDexClassLoader = (PathClassLoader) classLoader;
        baseDexClassLoader.addNativePath(libPaths);
    }

    @UnsupportedAppUsage
    private final ArrayMap<String, ClassLoader> mLoaders = new ArrayMap<>();

    private static final ApplicationLoaders gApplicationLoaders = new ApplicationLoaders();

    private static class CachedClassLoader {
        ClassLoader loader;

        /**
         * The shared libraries used when constructing loader for verification.
         */
        List<ClassLoader> sharedLibraries;
    }

    /**
     * This is a map of zip to associated class loader.
     */
    private Map<String, CachedClassLoader> mSystemLibsCacheMap = null;
}
