#filter substitution
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.mozglue;

import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Environment;
import android.util.Log;

import java.io.File;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.Locale;

public final class GeckoLoader {
    private static final String LOGTAG = "GeckoLoader";

    // This matches AppConstants, but we're built earlier.
    private static final String ANDROID_PACKAGE_NAME = "@ANDROID_PACKAGE_NAME@";

    private static volatile Intent sIntent;
    private static File sCacheFile;
    private static File sGREDir;

    private static final Object sLibLoadingLock = new Object();
    // Must hold sLibLoadingLock while accessing the following boolean variables.
    private static boolean sSQLiteLibsLoaded;
    private static boolean sNSSLibsLoaded;
    private static boolean sMozGlueLoaded;
    private static boolean sLibsSetup;

    private GeckoLoader() {
        // prevent instantiation
    }

    public static File getCacheDir(Context context) {
        if (sCacheFile == null) {
            sCacheFile = context.getCacheDir();
        }
        return sCacheFile;
    }

    public static File getGREDir(Context context) {
        if (sGREDir == null) {
            sGREDir = new File(context.getApplicationInfo().dataDir);
        }
        return sGREDir;
    }

    private static void setupPluginEnvironment(Context context, String[] pluginDirs) {
        // setup plugin path directories
        try {
            // Check to see if plugins were blocked.
            if (pluginDirs == null) {
                putenv("MOZ_PLUGINS_BLOCKED=1");
                putenv("MOZ_PLUGIN_PATH=");
                return;
            }

            StringBuilder pluginSearchPath = new StringBuilder();
            for (int i = 0; i < pluginDirs.length; i++) {
                pluginSearchPath.append(pluginDirs[i]);
                pluginSearchPath.append(":");
            }
            putenv("MOZ_PLUGIN_PATH="+pluginSearchPath);

            File pluginDataDir = context.getDir("plugins", 0);
            putenv("ANDROID_PLUGIN_DATADIR=" + pluginDataDir.getPath());

            File pluginPrivateDataDir = context.getDir("plugins_private", 0);
            putenv("ANDROID_PLUGIN_DATADIR_PRIVATE=" + pluginPrivateDataDir.getPath());

        } catch (Exception ex) {
            Log.w(LOGTAG, "Caught exception getting plugin dirs.", ex);
        }
    }

    private static void setupDownloadEnvironment(Context context) {
        try {
            File downloadDir = null;
            File updatesDir  = null;
            if (Build.VERSION.SDK_INT >= 8) {
                downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
                updatesDir  = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
            }
            if (downloadDir == null) {
                downloadDir = new File(Environment.getExternalStorageDirectory().getPath(), "download");
            }
            if (updatesDir == null) {
                updatesDir = downloadDir;
            }
            putenv("DOWNLOADS_DIRECTORY=" + downloadDir.getPath());
            putenv("UPDATES_DIRECTORY="   + updatesDir.getPath());
        }
        catch (Exception e) {
            Log.w(LOGTAG, "No download directory found.", e);
        }
    }

    private static void delTree(File file) {
        if (file.isDirectory()) {
            File children[] = file.listFiles();
            for (File child : children) {
                delTree(child);
            }
        }
        file.delete();
    }

    private static File getTmpDir(Context context) {
        File tmpDir = context.getDir("tmpdir", Context.MODE_PRIVATE);
        // check if the old tmp dir is there
        File oldDir = new File(tmpDir.getParentFile(), "app_tmp");
        if (oldDir.exists()) {
            delTree(oldDir);
        }
        return tmpDir;
    }

    public static void setLastIntent(Intent intent) {
        sIntent = intent;
    }

    public static void setupGeckoEnvironment(Context context, String[] pluginDirs, String profilePath) {
        // if we have an intent (we're being launched by an activity)
        // read in any environmental variables from it here
        final Intent intent = sIntent;
        if (intent != null) {
            String env = intent.getStringExtra("env0");
            Log.d(LOGTAG, "Gecko environment env0: " + env);
            for (int c = 1; env != null; c++) {
                putenv(env);
                env = intent.getStringExtra("env" + c);
                Log.d(LOGTAG, "env" + c + ": " + env);
            }
        }

        setupPluginEnvironment(context, pluginDirs);
        setupDownloadEnvironment(context);

        // profile home path
        putenv("HOME=" + profilePath);

        // setup the tmp path
        File f = getTmpDir(context);
        if (!f.exists()) {
            f.mkdirs();
        }
        putenv("TMPDIR=" + f.getPath());

        // setup the downloads path
        f = Environment.getDownloadCacheDirectory();
        putenv("EXTERNAL_STORAGE=" + f.getPath());

        // setup the app-specific cache path
        f = context.getCacheDir();
        putenv("CACHE_DIRECTORY=" + f.getPath());

        /* We really want to use this code, but it requires bumping up the SDK to 17 so for now
           we will use reflection. See https://bugzilla.mozilla.org/show_bug.cgi?id=811763#c11
        
        if (Build.VERSION.SDK_INT >= 17) {
            android.os.UserManager um = (android.os.UserManager)context.getSystemService(Context.USER_SERVICE);
            if (um != null) {
                putenv("MOZ_ANDROID_USER_SERIAL_NUMBER=" + um.getSerialNumberForUser(android.os.Process.myUserHandle()));
            } else {
                Log.d(LOGTAG, "Unable to obtain user manager service on a device with SDK version " + Build.VERSION.SDK_INT);
            }
        }
        */
        try {
            Object userManager = context.getSystemService("user");
            if (userManager != null) {
                // if userManager is non-null that means we're running on 4.2+ and so the rest of this
                // should just work
                Object userHandle = android.os.Process.class.getMethod("myUserHandle", (Class[])null).invoke(null);
                Object userSerial = userManager.getClass().getMethod("getSerialNumberForUser", userHandle.getClass()).invoke(userManager, userHandle);
                putenv("MOZ_ANDROID_USER_SERIAL_NUMBER=" + userSerial.toString());
            }
        } catch (Exception e) {
            // Guard against any unexpected failures
            Log.d(LOGTAG, "Unable to set the user serial number", e);
        }

        setupLocaleEnvironment();

        // We don't need this any more.
        sIntent = null;
    }

    private static void loadLibsSetup(Context context) {
        synchronized (sLibLoadingLock) {
            if (sLibsSetup) {
                return;
            }
            sLibsSetup = true;
        }

        // The package data lib directory isn't placed in ld.so's
        // search path, so we have to manually load libraries that
        // libxul will depend on.  Not ideal.

        File cacheFile = getCacheDir(context);
        putenv("GRE_HOME=" + getGREDir(context).getPath());

        // setup the libs cache
        String linkerCache = System.getenv("MOZ_LINKER_CACHE");
        if (linkerCache == null) {
            linkerCache = cacheFile.getPath();
            putenv("MOZ_LINKER_CACHE=" + linkerCache);
        }

        // Disable on-demand decompression of the linker on devices where it
        // is known to cause crashes.
        if ("HTC".equals(android.os.Build.MANUFACTURER) &&
            "HTC Vision".equals(android.os.Build.MODEL)) {
            putenv("MOZ_LINKER_ONDEMAND=0");
        }

#ifdef MOZ_LINKER_EXTRACT
        putenv("MOZ_LINKER_EXTRACT=1");
        // Ensure that the cache dir is world-writable
        File cacheDir = new File(linkerCache);
        if (cacheDir.isDirectory()) {
            cacheDir.setWritable(true, false);
            cacheDir.setExecutable(true, false);
            cacheDir.setReadable(true, false);
        }
#endif
    }

    @RobocopTarget
    public static void loadSQLiteLibs(Context context, String apkName) {
        synchronized (sLibLoadingLock) {
            if (sSQLiteLibsLoaded) {
                return;
            }
            sSQLiteLibsLoaded = true;
        }

        loadMozGlue();
        // the extract libs parameter is being removed in bug 732069
        loadLibsSetup(context);
        loadSQLiteLibsNative(apkName, false);
    }

    public static void loadNSSLibs(Context context, String apkName) {
        synchronized (sLibLoadingLock) {
            if (sNSSLibsLoaded) {
                return;
            }
            sNSSLibsLoaded = true;
        }

        loadMozGlue();
        loadLibsSetup(context);
        loadNSSLibsNative(apkName, false);
    }

    public static void doLoadLibrary(final String lib) {
        try {
            System.loadLibrary(lib);
        } catch (UnsatisfiedLinkError e) {
            Log.wtf(LOGTAG, "Couldn't load " + lib + ". Trying /data/app-lib path.");
            try {
                System.load("/data/app-lib/" + ANDROID_PACKAGE_NAME + "/lib" + lib + ".so");
            } catch (Throwable ee) {
                try {
                    Log.wtf(LOGTAG, "Couldn't load " + lib + ": " + ee + ". Trying /data/data path.");
                    System.load("/data/data/" + ANDROID_PACKAGE_NAME + "/lib/lib" + lib + ".so");
                } catch (Throwable eee) {
                    Log.wtf(LOGTAG, "Failed every attempt to load " + lib + ". Giving up.");
                    throw new RuntimeException("Unable to load " + lib, eee);
                }
            }
        }
    }

    public static void loadMozGlue() {
        synchronized (sLibLoadingLock) {
            if (sMozGlueLoaded) {
                return;
            }
            sMozGlueLoaded = true;
        }

        doLoadLibrary("mozglue");
    }

    public static void loadGeckoLibs(Context context, String apkName) {
        loadLibsSetup(context);
        loadGeckoLibsNative(apkName);
    }

    private static void setupLocaleEnvironment() {
        putenv("LANG=" + Locale.getDefault().toString());
        NumberFormat nf = NumberFormat.getInstance();
        if (nf instanceof DecimalFormat) {
            DecimalFormat df = (DecimalFormat)nf;
            DecimalFormatSymbols dfs = df.getDecimalFormatSymbols();

            putenv("LOCALE_DECIMAL_POINT=" + dfs.getDecimalSeparator());
            putenv("LOCALE_THOUSANDS_SEP=" + dfs.getGroupingSeparator());
            putenv("LOCALE_GROUPING=" + (char)df.getGroupingSize());
        }
    }

    // These methods are implemented in mozglue/android/nsGeckoUtils.cpp
    private static native void putenv(String map);

    // These methods are implemented in mozglue/android/APKOpen.cpp
    public static native void nativeRun(String args);
    private static native void loadGeckoLibsNative(String apkName);
    private static native void loadSQLiteLibsNative(String apkName, boolean shouldExtract);
    private static native void loadNSSLibsNative(String apkName, boolean shouldExtract);
}
