/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * 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 com.facebook.soloader;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.os.StrictMode;
import android.text.TextUtils;
import android.util.Log;
import com.facebook.soloader.nativeloader.NativeLoader;
import dalvik.system.BaseDexClassLoader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.NotThreadSafe;
import javax.annotation.concurrent.ThreadSafe;

/**
 * Native code loader.
 *
 * <p>To load a native library, call the static method {@link #loadLibrary} from the static
 * initializer of the Java class declaring the native methods. The argument should be the library's
 * short name.
 *
 * <p>For example, if the native code is in libmy_jni_methods.so:
 *
 * <pre>{@code
 * class MyClass {
 *   static {
 *     SoLoader.loadLibrary("my_jni_methods");
 *   }
 * }
 * }</pre>
 *
 * <p>Before any library can be loaded SoLoader needs to be initialized. The application using
 * SoLoader should do that by calling SoLoader.init early on app initialization path. The call must
 * happen before any class using SoLoader in its static initializer is loaded.
 */
@ThreadSafe
public class SoLoader {

  /* package */ static final String TAG = "SoLoader";
  /* package */ static final boolean DEBUG = false;
  /* package */ static final boolean SYSTRACE_LIBRARY_LOADING;
  /* package */ @Nullable static SoFileLoader sSoFileLoader;

  /**
   * locking controlling the list of SoSources. We want to allow long running iterations over the
   * list to happen concurrently, but also ensure that nothing modifies the list while others are
   * reading it.
   */
  private static final ReentrantReadWriteLock sSoSourcesLock = new ReentrantReadWriteLock();

  /**
   * Ordered list of sources to consult when trying to load a shared library or one of its
   * dependencies. {@code null} indicates that SoLoader is uninitialized.
   */
  @GuardedBy("sSoSourcesLock")
  @Nullable
  private static SoSource[] sSoSources = null;

  @GuardedBy("sSoSourcesLock")
  private static volatile int sSoSourcesVersion = 0;

  /** A backup SoSources to try if a lib file is corrupted */
  @GuardedBy("sSoSourcesLock")
  @Nullable
  private static UnpackingSoSource[] sBackupSoSources;

  /**
   * A SoSource for the Context.ApplicationInfo.nativeLibsDir that can be updated if the application
   * moves this directory
   */
  @GuardedBy("sSoSourcesLock")
  @Nullable
  private static ApplicationSoSource sApplicationSoSource;

  /** Records the sonames (e.g., "libdistract.so") of shared libraries we've loaded. */
  @GuardedBy("SoLoader.class")
  private static final HashSet<String> sLoadedLibraries = new HashSet<>();

  /**
   * Libraries that are in the process of being loaded, and lock objects to synchronize on and wait
   * for the loading to end.
   */
  @GuardedBy("SoLoader.class")
  private static final Map<String, Object> sLoadingLibraries = new HashMap<>();

  private static final Set<String> sLoadedAndMergedLibraries =
      Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());

  /** Wrapper for System.loadLibrary. */
  @Nullable private static SystemLoadLibraryWrapper sSystemLoadLibraryWrapper = null;

  /**
   * Name of the directory we use for extracted DSOs from built-in SO sources (main APK, exopackage)
   */
  private static final String SO_STORE_NAME_MAIN = "lib-main";

  /** Name of the directory we use for extracted DSOs from split APKs */
  private static final String SO_STORE_NAME_SPLIT = "lib-";

  /** Enable the exopackage SoSource. */
  public static final int SOLOADER_ENABLE_EXOPACKAGE = (1 << 0);

  /**
   * Allow deferring some initialization work to asynchronous background threads. Shared libraries
   * are nevertheless ready to load as soon as init returns.
   */
  public static final int SOLOADER_ALLOW_ASYNC_INIT = (1 << 1);

  public static final int SOLOADER_LOOK_IN_ZIP = (1 << 2);

  /**
   * In some contexts, using a backup so source in case of so corruption is not feasible e.g. lack
   * of write permissions to the library path.
   */
  public static final int SOLOADER_DISABLE_BACKUP_SOSOURCE = (1 << 3);

  /**
   * Skip calling JNI_OnLoad if the library is merged. This is necessary for libraries that don't
   * define JNI_OnLoad and are only loaded for their side effects (like static constructors
   * registering callbacks). DO NOT use this to allow implicit JNI registration (by naming your
   * methods Java_com_facebook_whatever) because that is buggy on Android.
   */
  public static final int SOLOADER_SKIP_MERGED_JNI_ONLOAD = (1 << 4);

  /** System Apps ignore PREFER_ANDROID_LIBS_DIRECTORY. Don't do that for this app. */
  public static final int SOLOADER_DONT_TREAT_AS_SYSTEMAPP = (1 << 5);

  @GuardedBy("sSoSourcesLock")
  private static int sFlags;

  private static boolean isSystemApp;

  static {
    boolean shouldSystrace = false;
    try {
      shouldSystrace = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2;
    } catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
      // In some test contexts, the Build class and/or some of its dependencies do not exist.
    }

    SYSTRACE_LIBRARY_LOADING = shouldSystrace;
  }

  public static void init(Context context, int flags) throws IOException {
    init(context, flags, null);
  }

  /**
   * Initializes native code loading for this app; this class's other static facilities cannot be
   * used until this {@link #init} is called. This method is idempotent: calls after the first are
   * ignored.
   *
   * @param context application context.
   * @param flags Zero or more of the SOLOADER_* flags
   * @param soFileLoader
   */
  public static void init(Context context, int flags, @Nullable SoFileLoader soFileLoader)
      throws IOException {
    StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
    try {
      isSystemApp = checkIfSystemApp(context, flags);
      initSoLoader(soFileLoader);
      initSoSources(context, flags, soFileLoader);
      if (!NativeLoader.isInitialized()) {
        NativeLoader.init(new NativeLoaderToSoLoaderDelegate());
      }
    } finally {
      StrictMode.setThreadPolicy(oldPolicy);
    }
  }

  /** Backward compatibility */
  public static void init(Context context, boolean nativeExopackage) {
    try {
      init(context, nativeExopackage ? SOLOADER_ENABLE_EXOPACKAGE : 0);
    } catch (IOException ex) {
      throw new RuntimeException(ex);
    }
  }

  private static void initSoSources(Context context, int flags, @Nullable SoFileLoader soFileLoader)
      throws IOException {
    sSoSourcesLock.writeLock().lock();
    try {
      if (sSoSources == null) {
        Log.d(TAG, "init start");
        sFlags = flags;

        ArrayList<SoSource> soSources = new ArrayList<>();

        //
        // Add SoSource objects for each of the system library directories.
        //

        String LD_LIBRARY_PATH = System.getenv("LD_LIBRARY_PATH");
        if (LD_LIBRARY_PATH == null) {
          LD_LIBRARY_PATH =
              SysUtil.is64Bit() ? "/vendor/lib64:/system/lib64" : "/vendor/lib:/system/lib";
        }

        for (String systemLibraryDirectory : LD_LIBRARY_PATH.split(":")) {
          // Don't pass DirectorySoSource.RESOLVE_DEPENDENCIES for directories we find on
          // LD_LIBRARY_PATH: Bionic's dynamic linker is capable of correctly resolving dependencies
          // these libraries have on each other, so doing that ourselves would be a waste.
          Log.d(TAG, "adding system library source: " + systemLibraryDirectory);
          File systemSoDirectory = new File(systemLibraryDirectory);
          soSources.add(
              new DirectorySoSource(systemSoDirectory, DirectorySoSource.ON_LD_LIBRARY_PATH));
        }

        //
        // We can only proceed forward if we have a Context. The prominent case
        // where we don't have a Context is barebones dalvikvm instantiations. In
        // that case, the caller is responsible for providing a correct LD_LIBRARY_PATH.
        //

        if (context != null) {
          //
          // Prepend our own SoSource for our own DSOs.
          //

          if ((flags & SOLOADER_ENABLE_EXOPACKAGE) != 0) {
            sBackupSoSources = null;
            Log.d(TAG, "adding exo package source: " + SO_STORE_NAME_MAIN);
            soSources.add(0, new ExoSoSource(context, SO_STORE_NAME_MAIN));
          } else {
            int apkSoSourceFlags;
            if (isSystemApp) {
              apkSoSourceFlags = 0;
            } else {
              apkSoSourceFlags = ApkSoSource.PREFER_ANDROID_LIBS_DIRECTORY;
              int ourSoSourceFlags = 0;

              // On old versions of Android, Bionic doesn't add our library directory to its
              // internal search path, and the system doesn't resolve dependencies between
              // modules we ship. On these systems, we resolve dependencies ourselves. On other
              // systems, Bionic's built-in resolver suffices.

              if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                ourSoSourceFlags |= DirectorySoSource.RESOLVE_DEPENDENCIES;
              }

              sApplicationSoSource = new ApplicationSoSource(context, ourSoSourceFlags);
              Log.d(TAG, "adding application source: " + sApplicationSoSource.toString());
              soSources.add(0, sApplicationSoSource);
            }

            if ((sFlags & SOLOADER_DISABLE_BACKUP_SOSOURCE) != 0) {
              sBackupSoSources = null;
            } else {

              final File mainApkDir = new File(context.getApplicationInfo().sourceDir);
              ArrayList<UnpackingSoSource> backupSources = new ArrayList<>();
              ApkSoSource mainApkSource =
                  new ApkSoSource(context, mainApkDir, SO_STORE_NAME_MAIN, apkSoSourceFlags);
              backupSources.add(mainApkSource);
              Log.d(TAG, "adding backup source from : " + mainApkSource.toString());

              if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
                  && context.getApplicationInfo().splitSourceDirs != null) {
                Log.d(TAG, "adding backup sources from split apks");
                int splitIndex = 0;
                for (String splitApkDir : context.getApplicationInfo().splitSourceDirs) {
                  ApkSoSource splitApkSource =
                      new ApkSoSource(
                          context,
                          new File(splitApkDir),
                          SO_STORE_NAME_SPLIT + (splitIndex++),
                          apkSoSourceFlags);
                  Log.d(TAG, "adding backup source: " + splitApkSource.toString());
                  backupSources.add(splitApkSource);
                }
              }

              sBackupSoSources = backupSources.toArray(new UnpackingSoSource[backupSources.size()]);
              soSources.addAll(0, backupSources);
            }
          }
        }

        SoSource[] finalSoSources = soSources.toArray(new SoSource[soSources.size()]);
        int prepareFlags = makePrepareFlags();
        for (int i = finalSoSources.length; i-- > 0; ) {
          Log.d(TAG, "Preparing SO source: " + finalSoSources[i]);
          finalSoSources[i].prepare(prepareFlags);
        }
        sSoSources = finalSoSources;
        sSoSourcesVersion++;
        Log.d(TAG, "init finish: " + sSoSources.length + " SO sources prepared");
      }
    } finally {
      Log.d(TAG, "init exiting");
      sSoSourcesLock.writeLock().unlock();
    }
  }

  private static int makePrepareFlags() {
    int prepareFlags = 0;
    // ensure the write lock is being held to protect sFlags
    // this is used when preparing new SoSources in the list.
    sSoSourcesLock.writeLock().lock();
    try {
      if ((sFlags & SOLOADER_ALLOW_ASYNC_INIT) != 0) {
        prepareFlags |= SoSource.PREPARE_FLAG_ALLOW_ASYNC_INIT;
      }
      return prepareFlags;
    } finally {
      sSoSourcesLock.writeLock().unlock();
    }
  }

  private static synchronized void initSoLoader(@Nullable SoFileLoader soFileLoader) {
    if (soFileLoader != null) {
      sSoFileLoader = soFileLoader;
      return;
    }

    final Runtime runtime = Runtime.getRuntime();
    final Method nativeLoadRuntimeMethod = getNativeLoadRuntimeMethod();

    final boolean hasNativeLoadMethod = nativeLoadRuntimeMethod != null;

    final String localLdLibraryPath =
        hasNativeLoadMethod ? Api14Utils.getClassLoaderLdLoadLibrary() : null;
    final String localLdLibraryPathNoZips = makeNonZipPath(localLdLibraryPath);

    sSoFileLoader =
        new SoFileLoader() {
          @Override
          public void load(final String pathToSoFile, final int loadFlags) {
            String error = null;
            if (hasNativeLoadMethod) {
              final boolean inZip = (loadFlags & SOLOADER_LOOK_IN_ZIP) == SOLOADER_LOOK_IN_ZIP;
              final String path = inZip ? localLdLibraryPath : localLdLibraryPathNoZips;
              try {
                synchronized (runtime) {
                  error =
                      (String)
                          nativeLoadRuntimeMethod.invoke(
                              runtime, pathToSoFile, SoLoader.class.getClassLoader(), path);
                  if (error != null) {
                    throw new UnsatisfiedLinkError(error);
                  }
                }
              } catch (IllegalAccessException
                  | IllegalArgumentException
                  | InvocationTargetException e) {
                error = "Error: Cannot load " + pathToSoFile;
                throw new RuntimeException(error, e);
              } finally {
                if (error != null) {
                  Log.e(
                      TAG,
                      "Error when loading lib: "
                          + error
                          + " lib hash: "
                          + getLibHash(pathToSoFile)
                          + " search path is "
                          + path);
                }
              }
            } else {
              System.load(pathToSoFile);
            }
          }

          /** * Logs MD5 of lib that failed loading */
          private String getLibHash(String libPath) {
            String digestStr;
            try {
              File libFile = new File(libPath);
              MessageDigest digest = MessageDigest.getInstance("MD5");
              try (InputStream libInStream = new FileInputStream(libFile)) {
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = libInStream.read(buffer)) > 0) {
                  digest.update(buffer, 0, bytesRead);
                }
                digestStr = String.format("%32x", new BigInteger(1, digest.digest()));
              }
            } catch (IOException e) {
              digestStr = e.toString();
            } catch (SecurityException e) {
              digestStr = e.toString();
            } catch (NoSuchAlgorithmException e) {
              digestStr = e.toString();
            }
            return digestStr;
          }
        };
  }

  @Nullable
  private static Method getNativeLoadRuntimeMethod() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Build.VERSION.SDK_INT > 27) {
      return null;
    }

    try {
      final Method method;
      method =
          Runtime.class.getDeclaredMethod(
              "nativeLoad", String.class, ClassLoader.class, String.class);

      method.setAccessible(true);
      return method;
    } catch (final NoSuchMethodException | SecurityException e) {
      Log.w(TAG, "Cannot get nativeLoad method", e);
      return null;
    }
  }

  private static boolean checkIfSystemApp(Context context, int flags) {
    if ((flags & SOLOADER_DONT_TREAT_AS_SYSTEMAPP) != 0) {
      return false;
    }

    return (context != null)
        && (context.getApplicationInfo().flags
                & (ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP))
            != 0;
  }

  /** Turn shared-library loading into a no-op. Useful in special circumstances. */
  public static void setInTestMode() {
    TestOnlyUtils.setSoSources(new SoSource[] {new NoopSoSource()});
  }

  /** Make shared-library loading delegate to the system. Useful for tests. */
  public static void deinitForTest() {
    TestOnlyUtils.setSoSources(null);
  }

  @NotThreadSafe
  static class TestOnlyUtils {
    /* Set so sources. Useful for tests. */
    /* package */ static void setSoSources(SoSource[] sources) {
      sSoSourcesLock.writeLock().lock();
      try {
        sSoSources = sources;
        sSoSourcesVersion++;
      } finally {
        sSoSourcesLock.writeLock().unlock();
      }
    }

    /**
     * Set so file loader. <br>
     * N.B. <b>ONLY FOR TESTS</b>. It has read/write race with {@code
     * SoLoader.sSoFileLoader.load(String, int)} in {@link DirectorySoSource#loadLibraryFrom}
     *
     * @param loader {@link SoFileLoader}
     */
    /* package */ static void setSoFileLoader(SoFileLoader loader) {
      sSoFileLoader = loader;
    }

    /** Reset internal status. Only for tests. */
    /* package */ static void resetStatus() {
      synchronized (SoLoader.class) {
        sLoadedLibraries.clear();
        sLoadingLibraries.clear();
        sSoFileLoader = null;
      }
      setSoSources(null);
    }
  }

  /**
   * Provide a wrapper object for calling {@link System#loadLibrary}. This is useful for controlling
   * which ClassLoader libraries are loaded into.
   */
  public static void setSystemLoadLibraryWrapper(SystemLoadLibraryWrapper wrapper) {
    sSystemLoadLibraryWrapper = wrapper;
  }

  public static final class WrongAbiError extends UnsatisfiedLinkError {
    WrongAbiError(Throwable cause, String machine) {
      super(
          "APK was built for a different platform. Supported ABIs: "
              + Arrays.toString(SysUtil.getSupportedAbis())
              + " error: "
              + machine);
      initCause(cause);
    }
  }

  /**
   * Gets the full path of a library.
   *
   * @param libName the library file name, including the prefix and extension.
   * @return the full path of the library, or null if it is not found in none of the SoSources.
   * @throws IOException if there is an error calculating the canonical path of libName
   */
  public static @Nullable String getLibraryPath(String libName) throws IOException {
    String libPath = null;

    sSoSourcesLock.readLock().lock();
    try {
      if (sSoSources != null) {
        for (int i = 0; libPath == null && i < sSoSources.length; ++i) {
          SoSource currentSource = sSoSources[i];
          libPath = currentSource.getLibraryPath(libName);
        }
      }
    } finally {
      sSoSourcesLock.readLock().unlock();
    }

    return libPath;
  }

  /**
   * Gets the dependencies of a library.
   *
   * @param libName the library file name, including the prefix and extension.
   * @return An array naming the dependencies of the library, or null if it is not found in any
   *     SoSources
   * @throws IOException if there is an error reading libName
   */
  public static @Nullable String[] getLibraryDependencies(String libName) throws IOException {
    String[] deps = null;

    sSoSourcesLock.readLock().lock();
    try {
      if (sSoSources != null) {
        for (int i = 0; deps == null && i < sSoSources.length; ++i) {
          SoSource currentSource = sSoSources[i];
          deps = currentSource.getLibraryDependencies(libName);
        }
      }
    } finally {
      sSoSourcesLock.readLock().unlock();
    }

    return deps;
  }

  public static boolean loadLibrary(String shortName) {
    return loadLibrary(shortName, 0);
  }

  /**
   * Load a shared library, initializing any JNI binding it contains.
   *
   * @param shortName Name of library to find, without "lib" prefix or ".so" suffix
   * @param loadFlags Control flags for the loading behavior. See available flags under {@link
   *     SoSource} (LOAD_FLAG_XXX).
   * @return Whether the library was loaded as a result of this call (true), or was already loaded
   *     through a previous call to SoLoader (false).
   */
  public static boolean loadLibrary(String shortName, int loadFlags) throws UnsatisfiedLinkError {
    sSoSourcesLock.readLock().lock();
    try {
      if (sSoSources == null) {
        // This should never happen during normal operation,
        // but if we're running in a non-Android environment,
        // fall back to System.loadLibrary.
        if ("http://www.android.com/".equals(System.getProperty("java.vendor.url"))) {
          // This will throw.
          assertInitialized();
        } else {
          // Not on an Android system.  Ask the JVM to load for us.
          synchronized (SoLoader.class) {
            boolean needsLoad = !sLoadedLibraries.contains(shortName);
            if (needsLoad) {
              if (sSystemLoadLibraryWrapper != null) {
                sSystemLoadLibraryWrapper.loadLibrary(shortName);
              } else {
                System.loadLibrary(shortName);
              }
            }
            return needsLoad;
          }
        }
      }
    } finally {
      sSoSourcesLock.readLock().unlock();
    }

    // This is to account for the fact that we want to load .so files from the apk itself when it is
    // a system app.
    if (isSystemApp && sSystemLoadLibraryWrapper != null) {
      sSystemLoadLibraryWrapper.loadLibrary(shortName);
      return true;
    }

    String mergedLibName = MergedSoMapping.mapLibName(shortName);

    String soName = mergedLibName != null ? mergedLibName : shortName;

    return loadLibraryBySoName(
        System.mapLibraryName(soName), shortName, mergedLibName, loadFlags, null);
  }

  /* package */ static void loadLibraryBySoName(
      String soName, int loadFlags, StrictMode.ThreadPolicy oldPolicy) {
    loadLibraryBySoNameImpl(soName, null, null, loadFlags, oldPolicy);
  }

  private static boolean loadLibraryBySoName(
      String soName,
      @Nullable String shortName,
      @Nullable String mergedLibName,
      int loadFlags,
      @Nullable StrictMode.ThreadPolicy oldPolicy) {
    boolean ret = false;
    boolean retry;
    do {
      retry = false;
      try {
        ret = loadLibraryBySoNameImpl(soName, shortName, mergedLibName, loadFlags, oldPolicy);
      } catch (UnsatisfiedLinkError e) {
        final int currentVersion = sSoSourcesVersion;
        sSoSourcesLock.writeLock().lock();
        try {
          if (sApplicationSoSource != null && sApplicationSoSource.checkAndMaybeUpdate()) {
            Log.w(
                TAG,
                "sApplicationSoSource updated during load: " + soName + ", attempting load again.");
            sSoSourcesVersion++;
            retry = true;
          }
        } catch (IOException ex) {
          throw new RuntimeException(ex);
        } finally {
          sSoSourcesLock.writeLock().unlock();
        }

        if (sSoSourcesVersion == currentVersion) {
          // nothing changed in soSource, Propagate original error
          throw e;
        }
      }
    } while (retry);
    return ret;
  }

  private static boolean loadLibraryBySoNameImpl(
      String soName,
      @Nullable String shortName,
      @Nullable String mergedLibName,
      int loadFlags,
      @Nullable StrictMode.ThreadPolicy oldPolicy) {
    // While we trust the JNI merging code (`invokeJniOnload(..)` below) to prevent us from invoking
    // JNI_OnLoad more than once and acknowledge it's more memory-efficient than tracking in Java,
    // by tracking loaded and merged libs in Java we can avoid undue synchronization and blocking
    // waits (which may occur on the UI thread and thus trigger ANRs).
    if (!TextUtils.isEmpty(shortName) && sLoadedAndMergedLibraries.contains(shortName)) {
      return false;
    }

    Object loadingLibLock;
    boolean loaded = false;
    synchronized (SoLoader.class) {
      if (sLoadedLibraries.contains(soName)) {
        if (mergedLibName == null) {
          // Not a merged lib, no need to init
          return false;
        }
        loaded = true;
      }
      if (sLoadingLibraries.containsKey(soName)) {
        loadingLibLock = sLoadingLibraries.get(soName);
      } else {
        loadingLibLock = new Object();
        sLoadingLibraries.put(soName, loadingLibLock);
      }
    }

    synchronized (loadingLibLock) {
      if (!loaded) {
        synchronized (SoLoader.class) {
          if (sLoadedLibraries.contains(soName)) {
            // Library was successfully loaded by other thread while we waited
            if (mergedLibName == null) {
              // Not a merged lib, no need to init
              return false;
            }
            loaded = true;
          }
          // Else, load was not successful on other thread. We will try in this one.
        }

        if (!loaded) {
          try {
            Log.d(TAG, "About to load: " + soName);
            doLoadLibraryBySoName(soName, loadFlags, oldPolicy);
          } catch (UnsatisfiedLinkError ex) {
            String message = ex.getMessage();
            if (message != null && message.contains("unexpected e_machine:")) {
              String machine_msg = message.substring(message.lastIndexOf("unexpected e_machine:"));
              throw new WrongAbiError(ex, machine_msg);
            }
            throw ex;
          }
          synchronized (SoLoader.class) {
            Log.d(TAG, "Loaded: " + soName);
            sLoadedLibraries.add(soName);
          }
        }
      }

      if ((loadFlags & SOLOADER_SKIP_MERGED_JNI_ONLOAD) == 0) {
        boolean isAlreadyMerged =
            !TextUtils.isEmpty(shortName) && sLoadedAndMergedLibraries.contains(shortName);
        if (mergedLibName != null && !isAlreadyMerged) {
          if (SYSTRACE_LIBRARY_LOADING) {
            Api18TraceUtils.beginTraceSection("MergedSoMapping.invokeJniOnload[", shortName, "]");
          }
          try {
            Log.d(TAG, "About to merge: " + shortName + " / " + soName);
            MergedSoMapping.invokeJniOnload(shortName);
            sLoadedAndMergedLibraries.add(shortName);
          } catch (UnsatisfiedLinkError e) {
            // If you are seeing this exception, first make sure your library sets
            // allow_jni_merging=True.  Trying to merge a library without that
            // will trigger this error.  If that's already in place, you're probably
            // not defining JNI_OnLoad.  Calling SoLoader.loadLibrary on a library
            // that doesn't define JNI_OnLoad is a no-op when that library is not merged.
            // Once you enable merging, it throws an UnsatisfiedLinkError.
            // There are three main reasons a library might not define JNI_OnLoad,
            // and the solution depends on which case you have.
            // - You might be using implicit registration (native methods defined like
            //   `Java_com_facebook_Foo_bar(JNIEnv* env)`).  This is not safe on Android
            //   https://fb.workplace.com/groups/442333009148653/permalink/651212928260659/
            //   and is not compatible with FBJNI.  Stop doing it.  Use FBJNI registerNatives.
            // - You might have a C++-only library with no JNI bindings and no static
            //   initializers with side-effects.  You can just delete the loadLibrary call.
            // - You might have a C++-only library that needs to be loaded explicitly because
            //   it has static initializers whose side-effects are needed.  In that case,
            //   pass the SOLOADER_SKIP_MERGED_JNI_ONLOAD flag to loadLibrary.
            throw new RuntimeException(
                "Failed to call JNI_OnLoad from '"
                    + shortName
                    + "', which has been merged into '"
                    + soName
                    + "'.  See comment for details.",
                e);
          } finally {
            if (SYSTRACE_LIBRARY_LOADING) {
              Api18TraceUtils.endSection();
            }
          }
        }
      }
    }
    return !loaded;
  }

  /**
   * Unpack library and its dependencies, returning the location of the unpacked library file. All
   * non-system dependencies of the given library will either be on LD_LIBRARY_PATH or will be in
   * the same directory as the returned File.
   *
   * @param shortName Name of library to find, without "lib" prefix or ".so" suffix
   * @return Unpacked DSO location
   */
  public static File unpackLibraryAndDependencies(String shortName) throws UnsatisfiedLinkError {
    assertInitialized();
    try {
      return unpackLibraryBySoName(System.mapLibraryName(shortName));
    } catch (IOException ex) {
      throw new RuntimeException(ex);
    }
  }

  private static void doLoadLibraryBySoName(
      String soName, int loadFlags, @Nullable StrictMode.ThreadPolicy oldPolicy)
      throws UnsatisfiedLinkError {

    int result = SoSource.LOAD_RESULT_NOT_FOUND;
    sSoSourcesLock.readLock().lock();
    try {
      if (sSoSources == null) {
        Log.e(TAG, "Could not load: " + soName + " because no SO source exists");
        throw new UnsatisfiedLinkError("couldn't find DSO to load: " + soName);
      }
    } finally {
      sSoSourcesLock.readLock().unlock();
    }

    // This way, we set the thread policy only one per loadLibrary no matter how many
    // dependencies we load.  Each call to StrictMode.allowThreadDiskWrites allocates.
    boolean restoreOldPolicy = false;
    if (oldPolicy == null) {
      oldPolicy = StrictMode.allowThreadDiskReads();
      restoreOldPolicy = true;
    }

    if (SYSTRACE_LIBRARY_LOADING) {
      Api18TraceUtils.beginTraceSection("SoLoader.loadLibrary[", soName, "]");
    }

    Throwable error = null;
    try {
      sSoSourcesLock.readLock().lock();
      try {
        for (int i = 0; result == SoSource.LOAD_RESULT_NOT_FOUND && i < sSoSources.length; ++i) {
          SoSource currentSource = sSoSources[i];
          result = currentSource.loadLibrary(soName, loadFlags, oldPolicy);
          if (result == SoSource.LOAD_RESULT_CORRUPTED_LIB_FILE && sBackupSoSources != null) {
            // Let's try from the backup source
            Log.d(TAG, "Trying backup SoSource for " + soName);
            for (UnpackingSoSource backupSoSource : sBackupSoSources) {
              backupSoSource.prepare(soName);
              int resultFromBackup = backupSoSource.loadLibrary(soName, loadFlags, oldPolicy);
              if (resultFromBackup == SoSource.LOAD_RESULT_LOADED) {
                result = resultFromBackup;
                break;
              }
            }
            break;
          }
        }
      } finally {
        sSoSourcesLock.readLock().unlock();
      }
    } catch (Throwable t) {
      error = t;
    } finally {
      if (SYSTRACE_LIBRARY_LOADING) {
        Api18TraceUtils.endSection();
      }

      if (restoreOldPolicy) {
        StrictMode.setThreadPolicy(oldPolicy);
      }
      if (result == SoSource.LOAD_RESULT_NOT_FOUND
          || result == SoSource.LOAD_RESULT_CORRUPTED_LIB_FILE) {
        StringBuilder sb = new StringBuilder().append("couldn't find DSO to load: ").append(soName);
        if (error != null) {
          String cause = error.getMessage();
          if (cause == null) {
            cause = error.toString();
          }
          sb.append(" caused by: ").append(cause);
          error.printStackTrace();
        } else {
          // load failure wasn't caused by dependent libraries.
          // Print the sources and current native library directory
          sSoSourcesLock.readLock().lock();
          for (int i = 0; i < sSoSources.length; ++i) {
            sb.append("\n\tSoSource ").append(i).append(": ").append(sSoSources[i].toString());
          }
          if (sApplicationSoSource != null) {
            Context updatedContext = sApplicationSoSource.getUpdatedContext();
            File updatedNativeLibDir =
                ApplicationSoSource.getNativeLibDirFromContext(updatedContext);
            sb.append("\n\tNative lib dir: ")
                .append(updatedNativeLibDir.getAbsolutePath())
                .append("\n");
          }
          sSoSourcesLock.readLock().unlock();
        }
        sb.append(" result: ").append(result);
        final String message = sb.toString();
        Log.e(TAG, message);
        UnsatisfiedLinkError err = new UnsatisfiedLinkError(message);
        if (error != null) {
          err.initCause(error);
        }
        throw err;
      }
    }
  }

  @Nullable
  public static String makeNonZipPath(final String localLdLibraryPath) {
    if (localLdLibraryPath == null) {
      return null;
    }

    final String[] paths = localLdLibraryPath.split(":");
    final ArrayList<String> pathsWithoutZip = new ArrayList<String>(paths.length);
    for (final String path : paths) {
      if (path.contains("!")) {
        continue;
      }
      pathsWithoutZip.add(path);
    }

    return TextUtils.join(":", pathsWithoutZip);
  }

  /* package */ static File unpackLibraryBySoName(String soName) throws IOException {
    sSoSourcesLock.readLock().lock();
    try {
      for (SoSource soSource : sSoSources) {
        File unpacked = soSource.unpackLibrary(soName);
        if (unpacked != null) {
          return unpacked;
        }
      }
    } finally {
      sSoSourcesLock.readLock().unlock();
    }

    throw new FileNotFoundException(soName);
  }

  private static void assertInitialized() {
    if (!isInitialized()) {
      throw new IllegalStateException("SoLoader.init() not yet called");
    }
  }

  public static boolean isInitialized() {
    sSoSourcesLock.readLock().lock();
    try {
      return sSoSources != null;
    } finally {
      sSoSourcesLock.readLock().unlock();
    }
  }

  public static int getSoSourcesVersion() {
    return sSoSourcesVersion;
  }

  /**
   * Add a new source of native libraries. SoLoader consults the new source before any
   * currently-installed source.
   *
   * @param extraSoSource The SoSource to install
   */
  public static void prependSoSource(SoSource extraSoSource) throws IOException {
    sSoSourcesLock.writeLock().lock();
    try {
      Log.d(TAG, "Prepending to SO sources: " + extraSoSource);
      assertInitialized();
      extraSoSource.prepare(makePrepareFlags());
      SoSource[] newSoSources = new SoSource[sSoSources.length + 1];
      newSoSources[0] = extraSoSource;
      System.arraycopy(sSoSources, 0, newSoSources, 1, sSoSources.length);
      sSoSources = newSoSources;
      sSoSourcesVersion++;
      Log.d(TAG, "Prepended to SO sources: " + extraSoSource);
    } finally {
      sSoSourcesLock.writeLock().unlock();
    }
  }

  /**
   * Retrieve an LD_LIBRARY_PATH value suitable for using the native linker to resolve our shared
   * libraries.
   */
  public static String makeLdLibraryPath() {
    sSoSourcesLock.readLock().lock();
    try {
      assertInitialized();
      Log.d(TAG, "makeLdLibraryPath");
      ArrayList<String> pathElements = new ArrayList<>();
      SoSource[] soSources = sSoSources;
      if (soSources != null) {
        for (SoSource soSource : soSources) {
          soSource.addToLdLibraryPath(pathElements);
        }
      }
      String joinedPaths = TextUtils.join(":", pathElements);
      Log.d(TAG, "makeLdLibraryPath final path: " + joinedPaths);
      return joinedPaths;
    } finally {
      sSoSourcesLock.readLock().unlock();
    }
  }

  /**
   * This function ensure that every SoSources Abi is supported for at least one abi in
   * SysUtil.getSupportedAbis
   *
   * @return true if all SoSources have their Abis supported
   */
  public static boolean areSoSourcesAbisSupported() {
    sSoSourcesLock.readLock().lock();
    try {
      if (sSoSources == null) {
        return false;
      }

      String[] supportedAbis = SysUtil.getSupportedAbis();
      for (SoSource soSource : sSoSources) {
        String[] soSourceAbis = soSource.getSoSourceAbis();
        for (String soSourceAbi : soSourceAbis) {
          boolean soSourceSupported = false;
          for (int k = 0; k < supportedAbis.length && !soSourceSupported; ++k) {
            soSourceSupported = soSourceAbi.equals(supportedAbis[k]);
          }
          if (!soSourceSupported) {
            Log.e(TAG, "abi not supported: " + soSourceAbi);
            return false;
          }
        }
      }

      return true;
    } finally {
      sSoSourcesLock.readLock().unlock();
    }
  }

  @DoNotOptimize
  @TargetApi(14)
  private static class Api14Utils {
    public static String getClassLoaderLdLoadLibrary() {
      final ClassLoader classLoader = SoLoader.class.getClassLoader();

      if (classLoader != null && !(classLoader instanceof BaseDexClassLoader)) {
        throw new IllegalStateException(
            "ClassLoader "
                + classLoader.getClass().getName()
                + " should be of type BaseDexClassLoader");
      }
      try {
        final BaseDexClassLoader baseDexClassLoader = (BaseDexClassLoader) classLoader;
        final Method getLdLibraryPathMethod =
            BaseDexClassLoader.class.getMethod("getLdLibraryPath");

        return (String) getLdLibraryPathMethod.invoke(baseDexClassLoader);
      } catch (Exception e) {
        throw new RuntimeException("Cannot call getLdLibraryPath", e);
      }
    }
  }
}