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

package org.chromium.base.library_loader;

import android.content.Context;
import android.util.Log;

import org.chromium.base.CommandLine;
import org.chromium.base.JNINamespace;
import org.chromium.base.SysUtils;
import org.chromium.base.TraceEvent;

/**
 * This class provides functionality to load and register the native libraries.
 * Callers are allowed to separate loading the libraries from initializing them.
 * This may be an advantage for Android Webview, where the libraries can be loaded
 * by the zygote process, but then needs per process initialization after the
 * application processes are forked from the zygote process.
 *
 * The libraries may be loaded and initialized from any thread. Synchronization
 * primitives are used to ensure that overlapping requests from different
 * threads are handled sequentially.
 *
 * See also base/android/library_loader/library_loader_hooks.cc, which contains
 * the native counterpart to this class.
 */
@JNINamespace("base::android")
public class LibraryLoader {
    private static final String TAG = "LibraryLoader";

    // Guards all access to the libraries
    private static final Object sLock = new Object();

    // One-way switch becomes true when the libraries are loaded.
    private static boolean sLoaded = false;

    // One-way switch becomes true when the libraries are initialized (
    // by calling nativeLibraryLoaded, which forwards to LibraryLoaded(...) in
    // library_loader_hooks.cc).
    private static boolean sInitialized = false;

    // One-way switch becomes true if the system library loading failed,
    // and the right native library was found and loaded by the hack.
    // The flag is used to report UMA stats later.
    private static boolean sNativeLibraryHackWasUsed = false;

    // TODO(cjhopman): Remove this once it's unused.
    /**
     * Doesn't do anything.
     */
    @Deprecated
    public static void setLibraryToLoad(String library) {
    }

    /**
     * TODO: http://crbug.com/354655
     * remove this method once WebViewChromiumFactoryProvider.java
     * changes the call to ensureInitialized(null).
     */
    public static void ensureInitialized() throws ProcessInitException {
        ensureInitialized(null);
    }

    /**
     *  This method blocks until the library is fully loaded and initialized.
     *
     *  @param context The context in which the method is called, the caller
     *    may pass in a null context if it doesn't know in which context it
     *    is running, or it doesn't need to work around the issue
     *    http://b/13216167.
     *
     *    When the context is not null and native library was not extracted
     *    by Android package manager, the LibraryLoader class
     *    will extract the native libraries from APK. This is a hack used to
     *    work around some Sony devices with the following platform bug:
     *    http://b/13216167.
     */
    public static void ensureInitialized(Context context) throws ProcessInitException {
        synchronized (sLock) {
            if (sInitialized) {
                // Already initialized, nothing to do.
                return;
            }
            loadAlreadyLocked(context);
            initializeAlreadyLocked(CommandLine.getJavaSwitchesOrNull());
        }
    }

    /**
     * Checks if library is fully loaded and initialized.
     */
    public static boolean isInitialized() {
        synchronized (sLock) {
            return sInitialized;
        }
    }

    /**
     * Loads the library and blocks until the load completes. The caller is responsible
     * for subsequently calling ensureInitialized().
     * May be called on any thread, but should only be called once. Note the thread
     * this is called on will be the thread that runs the native code's static initializers.
     * See the comment in doInBackground() for more considerations on this.
     *
     * @throws ProcessInitException if the native library failed to load.
     */
    public static void loadNow(Context context) throws ProcessInitException {
        synchronized (sLock) {
            loadAlreadyLocked(context);
        }
    }


    /**
     * initializes the library here and now: must be called on the thread that the
     * native will call its "main" thread. The library must have previously been
     * loaded with loadNow.
     * @param initCommandLine The command line arguments that native command line will
     * be initialized with.
     */
    public static void initialize(String[] initCommandLine) throws ProcessInitException {
        synchronized (sLock) {
            initializeAlreadyLocked(initCommandLine);
        }
    }


    // Invoke System.loadLibrary(...), triggering JNI_OnLoad in native code
    private static void loadAlreadyLocked(Context context) throws ProcessInitException {
        try {
            if (!sLoaded) {
                assert !sInitialized;

                long startTime = System.currentTimeMillis();
                boolean useChromiumLinker = Linker.isUsed();

                if (useChromiumLinker)
                    Linker.prepareLibraryLoad();

                for (String library : NativeLibraries.LIBRARIES) {
                    Log.i(TAG, "Loading: " + library);
                    if (useChromiumLinker)
                        Linker.loadLibrary(library);
                    else
                        try {
                            System.loadLibrary(library);
                            if (context != null) {
                                LibraryLoaderHelper.deleteWorkaroundLibrariesAsynchronously(
                                    context);
                            }
                        } catch (UnsatisfiedLinkError e) {
                            if (context != null
                                && LibraryLoaderHelper.tryLoadLibraryUsingWorkaround(context,
                                                                                     library)) {
                                sNativeLibraryHackWasUsed = true;
                            } else {
                                throw e;
                            }
                        }
                }
                if (useChromiumLinker)
                    Linker.finishLibraryLoad();
                long stopTime = System.currentTimeMillis();
                Log.i(TAG, String.format("Time to load native libraries: %d ms (timestamps %d-%d)",
                                         stopTime - startTime,
                                         startTime % 10000,
                                         stopTime % 10000));
                sLoaded = true;
            }
        } catch (UnsatisfiedLinkError e) {
            throw new ProcessInitException(LoaderErrors.LOADER_ERROR_NATIVE_LIBRARY_LOAD_FAILED, e);
        }
        // Check that the version of the library we have loaded matches the version we expect
        Log.i(TAG, String.format(
                "Expected native library version number \"%s\"," +
                        "actual native library version number \"%s\"",
                NativeLibraries.VERSION_NUMBER,
                nativeGetVersionNumber()));
        if (!NativeLibraries.VERSION_NUMBER.equals(nativeGetVersionNumber())) {
            throw new ProcessInitException(LoaderErrors.LOADER_ERROR_NATIVE_LIBRARY_WRONG_VERSION);
        }

    }


    // Invoke base::android::LibraryLoaded in library_loader_hooks.cc
    private static void initializeAlreadyLocked(String[] initCommandLine)
            throws ProcessInitException {
        if (sInitialized) {
            return;
        }
        if (!nativeLibraryLoaded(initCommandLine)) {
            Log.e(TAG, "error calling nativeLibraryLoaded");
            throw new ProcessInitException(LoaderErrors.LOADER_ERROR_FAILED_TO_REGISTER_JNI);
        }
        // From this point on, native code is ready to use and checkIsReady()
        // shouldn't complain from now on (and in fact, it's used by the
        // following calls).
        sInitialized = true;
        CommandLine.enableNativeProxy();
        TraceEvent.setEnabledToMatchNative();
        // Record histogram for the Chromium linker.
        if (Linker.isUsed())
            nativeRecordChromiumAndroidLinkerHistogram(Linker.loadAtFixedAddressFailed(),
                                                    SysUtils.isLowEndDevice());

        nativeRecordNativeLibraryHack(sNativeLibraryHackWasUsed);
    }

    // Only methods needed before or during normal JNI registration are during System.OnLoad.
    // nativeLibraryLoaded is then called to register everything else.  This process is called
    // "initialization".  This method will be mapped (by generated code) to the LibraryLoaded
    // definition in base/android/library_loader/library_loader_hooks.cc.
    //
    // Return true on success and false on failure.
    private static native boolean nativeLibraryLoaded(String[] initCommandLine);

    // Method called to record statistics about the Chromium linker operation,
    // i.e. whether the library failed to be loaded at a fixed address, and
    // whether the device is 'low-memory'.
    private static native void nativeRecordChromiumAndroidLinkerHistogram(
         boolean loadedAtFixedAddressFailed,
         boolean isLowMemoryDevice);

    // Get the version of the native library. This is needed so that we can check we
    // have the right version before initializing the (rest of the) JNI.
    private static native String nativeGetVersionNumber();

    private static native void nativeRecordNativeLibraryHack(boolean usedHack);
}
