/*
 * Copyright (C) 2016 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.webkit;

import android.app.LoadedApk;
import android.content.pm.PackageInfo;
import android.os.Build;
import android.os.SystemService;
import android.os.ZygoteProcess;
import android.text.TextUtils;
import android.util.AndroidRuntimeException;
import android.util.Log;

import com.android.internal.annotations.GuardedBy;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeoutException;

/** @hide */
public class WebViewZygote {
    private static final String LOGTAG = "WebViewZygote";

    private static final String WEBVIEW_ZYGOTE_SERVICE_32 = "webview_zygote32";
    private static final String WEBVIEW_ZYGOTE_SERVICE_64 = "webview_zygote64";
    private static final String WEBVIEW_ZYGOTE_SOCKET = "webview_zygote";

    /**
     * Lock object that protects all other static members.
     */
    private static final Object sLock = new Object();

    /**
     * Instance that maintains the socket connection to the zygote. This is null if the zygote
     * is not running or is not connected.
     */
    @GuardedBy("sLock")
    private static ZygoteProcess sZygote;

    /**
     * Variable that allows us to determine whether the WebView zygote Service has already been
     * started.
     */
    @GuardedBy("sLock")
    private static boolean sStartedService = false;

    /**
     * Information about the selected WebView package. This is set from #onWebViewProviderChanged().
     */
    @GuardedBy("sLock")
    private static PackageInfo sPackage;

    /**
     * Cache key for the selected WebView package's classloader. This is set from
     * #onWebViewProviderChanged().
     */
    @GuardedBy("sLock")
    private static String sPackageCacheKey;

    /**
     * Flag for whether multi-process WebView is enabled. If this is false, the zygote
     * will not be started.
     */
    @GuardedBy("sLock")
    private static boolean sMultiprocessEnabled = false;

    public static ZygoteProcess getProcess() {
        synchronized (sLock) {
            if (sZygote != null) return sZygote;

            waitForServiceStartAndConnect();
            return sZygote;
        }
    }

    public static String getPackageName() {
        synchronized (sLock) {
            return sPackage.packageName;
        }
    }

    public static boolean isMultiprocessEnabled() {
        synchronized (sLock) {
            return sMultiprocessEnabled && sPackage != null;
        }
    }

    public static void setMultiprocessEnabled(boolean enabled) {
        synchronized (sLock) {
            sMultiprocessEnabled = enabled;

            // When toggling between multi-process being on/off, start or stop the
            // service. If it is enabled and the zygote is not yet started, bring up the service.
            // Otherwise, bring down the service. The name may be null if the package
            // information has not yet been resolved.
            final String serviceName = getServiceNameLocked();
            if (serviceName == null) return;

            if (enabled) {
                if (!sStartedService) {
                    SystemService.start(serviceName);
                    sStartedService = true;
                }
            } else {
                SystemService.stop(serviceName);
                sStartedService = false;
                sZygote = null;
            }
        }
    }

    public static void onWebViewProviderChanged(PackageInfo packageInfo, String cacheKey) {
        synchronized (sLock) {
            sPackage = packageInfo;
            sPackageCacheKey = cacheKey;

            // If multi-process is not enabled, then do not start the zygote service.
            if (!sMultiprocessEnabled) {
                return;
            }

            final String serviceName = getServiceNameLocked();
            sZygote = null;

            // The service may enter the RUNNING state before it opens the socket,
            // so connectToZygoteIfNeededLocked() may still fail.
            if (SystemService.isStopped(serviceName)) {
                SystemService.start(serviceName);
            } else {
                SystemService.restart(serviceName);
            }
            sStartedService = true;
        }
    }

    private static void waitForServiceStartAndConnect() {
        if (!sStartedService) {
            throw new AndroidRuntimeException("Tried waiting for the WebView Zygote Service to " +
                    "start running without first starting the service.");
        }

        String serviceName;
        synchronized (sLock) {
            serviceName = getServiceNameLocked();
        }
        try {
            SystemService.waitForState(serviceName, SystemService.State.RUNNING, 5000);
        } catch (TimeoutException e) {
            Log.e(LOGTAG, "Timed out waiting for " + serviceName);
            return;
        }

        synchronized (sLock) {
            connectToZygoteIfNeededLocked();
        }
    }

    @GuardedBy("sLock")
    private static String getServiceNameLocked() {
        if (sPackage == null)
            return null;

        if (Arrays.asList(Build.SUPPORTED_64_BIT_ABIS).contains(
                    sPackage.applicationInfo.primaryCpuAbi)) {
            return WEBVIEW_ZYGOTE_SERVICE_64;
        }

        return WEBVIEW_ZYGOTE_SERVICE_32;
    }

    @GuardedBy("sLock")
    private static void connectToZygoteIfNeededLocked() {
        if (sZygote != null) {
            return;
        }

        if (sPackage == null) {
            Log.e(LOGTAG, "Cannot connect to zygote, no package specified");
            return;
        }

        final String serviceName = getServiceNameLocked();
        if (!SystemService.isRunning(serviceName)) {
            Log.e(LOGTAG, serviceName + " is not running");
            return;
        }

        try {
            sZygote = new ZygoteProcess(WEBVIEW_ZYGOTE_SOCKET, null);

            // All the work below is usually done by LoadedApk, but the zygote can't talk to
            // PackageManager or construct a LoadedApk since it's single-threaded pre-fork, so
            // doesn't have an ActivityThread and can't use Binder.
            // Instead, figure out the paths here, in the system server where we have access to
            // the package manager. Reuse the logic from LoadedApk to determine the correct
            // paths and pass them to the zygote as strings.
            final List<String> zipPaths = new ArrayList<>(10);
            final List<String> libPaths = new ArrayList<>(10);
            LoadedApk.makePaths(null, false, sPackage.applicationInfo, zipPaths, libPaths);
            final String librarySearchPath = TextUtils.join(File.pathSeparator, libPaths);
            final String zip = (zipPaths.size() == 1) ? zipPaths.get(0) :
                    TextUtils.join(File.pathSeparator, zipPaths);

            waitForZygote();

            Log.d(LOGTAG, "Preloading package " + zip + " " + librarySearchPath);
            sZygote.preloadPackageForAbi(zip, librarySearchPath, sPackageCacheKey,
                                         Build.SUPPORTED_ABIS[0]);
        } catch (Exception e) {
            Log.e(LOGTAG, "Error connecting to " + serviceName, e);
            sZygote = null;
        }
    }

    /**
     * Wait until a connection to the Zygote can be established.
     */
    private static void waitForZygote() {
        while (true) {
            try {
                final ZygoteProcess.ZygoteState zs =
                        ZygoteProcess.ZygoteState.connect(WEBVIEW_ZYGOTE_SOCKET);
                zs.close();
                break;
            } catch (IOException ioe) {
                Log.w(LOGTAG, "Got error connecting to zygote, retrying. msg= " + ioe.getMessage());
            }

            try {
                Thread.sleep(1000);
            } catch (InterruptedException ie) {
            }
        }
    }
}
