// 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.content.pm.ApplicationInfo;
import android.os.Build;
import android.util.Log;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * The class provides helper functions to extract native libraries from APK,
 * and load libraries from there.
 *
 * The class should be package-visible only, but made public for testing
 * purpose.
 */
public class LibraryLoaderHelper {
    private static final String TAG = "LibraryLoaderHelper";

    private static final String LIB_DIR = "lib";

    /**
     * One-way switch becomes true if native libraries were unpacked
     * from APK.
     */
    private static boolean sLibrariesWereUnpacked = false;

    /**
     * Loads native libraries using workaround only, skip the library in system
     * lib path. The method exists only for testing purpose.
     * Caller must ensure thread safety of this method.
     * @param context
     */
    public static boolean loadNativeLibrariesUsingWorkaroundForTesting(Context context) {
        // Although tryLoadLibraryUsingWorkaround might be called multiple times,
        // libraries should only be unpacked once, this is guaranteed by
        // sLibrariesWereUnpacked.
        for (String library : NativeLibraries.LIBRARIES) {
            if (!tryLoadLibraryUsingWorkaround(context, library)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Try to load a native library using a workaround of
     *   http://b/13216167.
     *
     * Workaround for b/13216167 was adapted from code in
     * https://googleplex-android-review.git.corp.google.com/#/c/433061
     *
     * More details about http://b/13216167:
     *   PackageManager may fail to update shared library.
     *
     * Native library directory in an updated package is a symbolic link
     * to a directory in /data/app-lib/<package name>, for example:
     * /data/data/com.android.chrome/lib -> /data/app-lib/com.android.chrome[-1].
     * When updating the application, the PackageManager create a new directory,
     * e.g., /data/app-lib/com.android.chrome-2, and remove the old symlink and
     * recreate one to the new directory. However, on some devices (e.g. Sony Xperia),
     * the symlink was updated, but fails to extract new native libraries from
     * the new apk.

     * We make the following changes to alleviate the issue:
     *  1) name the native library with apk version code, e.g.,
     *     libchrome.1750.136.so, 1750.136 is Chrome version number;
     *  2) first try to load the library using System.loadLibrary,
     *     if that failed due to the library file was not found,
     *     search the named library in a /data/data/com.android.chrome/app_lib
     *     directory. Because of change 1), each version has a different native
     *     library name, so avoid mistakenly using the old native library.
     *
     *  If named library is not in /data/data/com.android.chrome/app_lib directory,
     *  extract native libraries from apk and cache in the directory.
     *
     * This function doesn't throw UnsatisfiedLinkError, the caller needs to
     * check the return value.
     */
    static boolean tryLoadLibraryUsingWorkaround(Context context, String library) {
        assert context != null;
        File libFile = getWorkaroundLibFile(context, library);
        if (!libFile.exists() && !unpackLibrariesOnce(context)) {
            return false;
        }
        try {
            System.load(libFile.getAbsolutePath());
            return true;
        } catch (UnsatisfiedLinkError e) {
            return false;
        }
    }

    /**
     * Returns the directory for holding extracted native libraries.
     * It may create the directory if it doesn't exist.
     *
     * @param context
     * @return the directory file object
     */
    public static File getWorkaroundLibDir(Context context) {
        return context.getDir(LIB_DIR, Context.MODE_PRIVATE);
    }

    private static File getWorkaroundLibFile(Context context, String library) {
        String libName = System.mapLibraryName(library);
        return new File(getWorkaroundLibDir(context), libName);
    }

    /**
     * Unpack native libraries from the APK file. The method is supposed to
     * be called only once. It deletes existing files in unpacked directory
     * before unpacking.
     *
     * @param context
     * @return true when unpacking was successful, false when failed or called
     *         more than once.
     */
    private static boolean unpackLibrariesOnce(Context context) {
        if (sLibrariesWereUnpacked) {
            return false;
        }
        sLibrariesWereUnpacked = true;

        File libDir = getWorkaroundLibDir(context);
        if (libDir.exists()) {
            assert libDir.isDirectory();
            deleteDirectorySync(libDir);
        }

        try {
            ApplicationInfo appInfo = context.getApplicationInfo();
            ZipFile file = new ZipFile(new File(appInfo.sourceDir), ZipFile.OPEN_READ);
            for (String libName : NativeLibraries.LIBRARIES) {
                String jniNameInApk = "lib/" + Build.CPU_ABI + "/" +
                    System.mapLibraryName(libName);

                final ZipEntry entry = file.getEntry(jniNameInApk);
                if (entry == null) {
                    Log.e(TAG, appInfo.sourceDir + " doesn't have file " + jniNameInApk);
                    file.close();
                    deleteDirectorySync(libDir);
                    return false;
                }

                File outputFile = getWorkaroundLibFile(context, libName);

                Log.i(TAG, "Extracting native libraries into " + outputFile.getAbsolutePath());

                assert !outputFile.exists();

                try {
                    if (!outputFile.createNewFile()) {
                        throw new IOException();
                    }

                    InputStream is = null;
                    FileOutputStream os = null;
                    try {
                        is = file.getInputStream(entry);
                        os = new FileOutputStream(outputFile);
                        int count = 0;
                        byte[] buffer = new byte[16 * 1024];
                        while ((count = is.read(buffer)) > 0) {
                            os.write(buffer, 0, count);
                        }
                    } finally {
                        try {
                            if (is != null) is.close();
                        } finally {
                            if (os != null) os.close();
                        }
                    }
                    // Change permission to rwxr-xr-x
                    outputFile.setReadable(true, false);
                    outputFile.setExecutable(true, false);
                    outputFile.setWritable(true);
                } catch (IOException e) {
                    if (outputFile.exists()) {
                        if (!outputFile.delete()) {
                            Log.e(TAG, "Failed to delete " + outputFile.getAbsolutePath());
                        }
                    }
                    file.close();
                    throw e;
                }
            }
            file.close();
            return true;
        } catch (IOException e) {
            Log.e(TAG, "Failed to unpack native libraries", e);
            deleteDirectorySync(libDir);
            return false;
        }
    }

    /**
     * Delete old library files in the backup directory.
     *  The actual deletion is done in a background thread.
     *
     * @param context
     */
    static void deleteWorkaroundLibrariesAsynchronously(Context context) {
        // Child process should not reach here.
        final File libDir = getWorkaroundLibDir(context);
        if (libDir.exists()) {
            assert libDir.isDirectory();
            // Async deletion
            new Thread() {
                @Override
                public void run() {
                    deleteDirectorySync(libDir);
                }
            }.start();
        }
    }

    /**
     * Delete the workaround libraries and directory synchronously.
     * For testing purpose only.
     * @param context
     */
    public static void deleteWorkaroundLibrariesSynchronously(Context context) {
        File libDir = getWorkaroundLibDir(context);
        if (libDir.exists()) {
            deleteDirectorySync(libDir);
        }
    }

    private static void deleteDirectorySync(File dir) {
        try {
            File[] files = dir.listFiles();
            if (files != null) {
                for (File file : files) {
                    String fileName = file.getName();
                    if (!file.delete()) {
                      Log.e(TAG, "Failed to remove " + file.getAbsolutePath());
                    }
                }
            }
            if (!dir.delete()) {
                Log.w(TAG, "Failed to remove " + dir.getAbsolutePath());
            }
            return;
        } catch (Exception e) {
            Log.e(TAG, "Failed to remove old libs, ", e);
        }
    }
}
