/*
 * Copyright (C) 2018 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.server.wm;

import static androidx.test.InstrumentationRegistry.getInstrumentation;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;

import android.app.ActivityManager;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.nano.DeviceConfigurationProto;
import android.content.nano.GlobalConfigurationProto;
import android.content.nano.LocaleProto;
import android.content.nano.ResourcesConfigurationProto;
import android.content.pm.ConfigurationInfo;
import android.content.pm.FeatureInfo;
import android.content.pm.PackageManager;
import android.content.pm.SharedLibraryInfo;
import android.content.res.Configuration;
import android.hardware.display.DisplayManager;
import android.opengl.GLES10;
import android.os.Build;
import android.os.LocaleList;
import android.os.ParcelFileDescriptor;
import android.platform.test.annotations.Presubmit;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.Display;

import com.google.protobuf.nano.InvalidProtocolBufferNanoException;

import org.junit.Before;
import org.junit.Test;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.egl.EGLSurface;

@Presubmit
public class ActivityManagerGetConfigTests {
    Context mContext;
    ActivityManager mAm;
    PackageManager mPm;

    @Before
    public void setUp() throws Exception {
        mContext = getInstrumentation().getTargetContext();
        mAm = mContext.getSystemService(ActivityManager.class);
        mPm = mContext.getPackageManager();
    }

    private byte[] executeShellCommand(String cmd) {
        try {
            ParcelFileDescriptor pfd = getInstrumentation().getUiAutomation()
                    .executeShellCommand(cmd);
            byte[] buf = new byte[512];
            int bytesRead;
            FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
            ByteArrayOutputStream stdout = new ByteArrayOutputStream();
            while ((bytesRead = fis.read(buf)) != -1) {
                stdout.write(buf, 0, bytesRead);
            }
            fis.close();
            return stdout.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Adds all supported GL extensions for a provided EGLConfig to a set by creating an EGLContext
     * and EGLSurface and querying extensions.
     *
     * @param egl An EGL API object
     * @param display An EGLDisplay to create a context and surface with
     * @param config The EGLConfig to get the extensions for
     * @param surfaceSize eglCreatePbufferSurface generic parameters
     * @param contextAttribs eglCreateContext generic parameters
     * @param glExtensions A Set<String> to add GL extensions to
     */
    private static void addExtensionsForConfig(
            EGL10 egl,
            EGLDisplay display,
            EGLConfig config,
            int[] surfaceSize,
            int[] contextAttribs,
            Set<String> glExtensions) {
        // Create a context.
        EGLContext context =
                egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, contextAttribs);
        // No-op if we can't create a context.
        if (context == EGL10.EGL_NO_CONTEXT) {
            return;
        }

        // Create a surface.
        EGLSurface surface = egl.eglCreatePbufferSurface(display, config, surfaceSize);
        if (surface == EGL10.EGL_NO_SURFACE) {
            egl.eglDestroyContext(display, context);
            return;
        }

        // Update the current surface and context.
        egl.eglMakeCurrent(display, surface, surface, context);

        // Get the list of extensions.
        String extensionList = GLES10.glGetString(GLES10.GL_EXTENSIONS);
        if (!TextUtils.isEmpty(extensionList)) {
            // The list of extensions comes from the driver separated by spaces.
            // Split them apart and add them into a Set for deduping purposes.
            for (String extension : extensionList.split(" ")) {
                glExtensions.add(extension);
            }
        }

        // Tear down the context and surface for this config.
        egl.eglMakeCurrent(display, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
        egl.eglDestroySurface(display, surface);
        egl.eglDestroyContext(display, context);
    }


    Set<String> getGlExtensionsFromDriver() {
        Set<String> glExtensions = new HashSet<>();

        // Get the EGL implementation.
        EGL10 egl = (EGL10) EGLContext.getEGL();
        if (egl == null) {
            throw new RuntimeException("Warning: couldn't get EGL");
        }

        // Get the default display and initialize it.
        EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
        int[] version = new int[2];
        egl.eglInitialize(display, version);

        // Call getConfigs() in order to find out how many there are.
        int[] numConfigs = new int[1];
        if (!egl.eglGetConfigs(display, null, 0, numConfigs)) {
            throw new RuntimeException("Warning: couldn't get EGL config count");
        }

        // Allocate space for all configs and ask again.
        EGLConfig[] configs = new EGLConfig[numConfigs[0]];
        if (!egl.eglGetConfigs(display, configs, numConfigs[0], numConfigs)) {
            throw new RuntimeException("Warning: couldn't get EGL configs");
        }

        // Allocate surface size parameters outside of the main loop to cut down
        // on GC thrashing.  1x1 is enough since we are only using it to get at
        // the list of extensions.
        int[] surfaceSize =
                new int[] {
                        EGL10.EGL_WIDTH, 1,
                        EGL10.EGL_HEIGHT, 1,
                        EGL10.EGL_NONE
                };

        // For when we need to create a GLES2.0 context.
        final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
        int[] gles2 = new int[] {EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE};

        // For getting return values from eglGetConfigAttrib
        int[] attrib = new int[1];

        for (int i = 0; i < numConfigs[0]; i++) {
            // Get caveat for this config in order to skip slow (i.e. software) configs.
            egl.eglGetConfigAttrib(display, configs[i], EGL10.EGL_CONFIG_CAVEAT, attrib);
            if (attrib[0] == EGL10.EGL_SLOW_CONFIG) {
                continue;
            }

            // If the config does not support pbuffers we cannot do an eglMakeCurrent
            // on it in addExtensionsForConfig(), so skip it here. Attempting to make
            // it current with a pbuffer will result in an EGL_BAD_MATCH error
            egl.eglGetConfigAttrib(display, configs[i], EGL10.EGL_SURFACE_TYPE, attrib);
            if ((attrib[0] & EGL10.EGL_PBUFFER_BIT) == 0) {
                continue;
            }

            final int EGL_OPENGL_ES_BIT = 0x0001;
            final int EGL_OPENGL_ES2_BIT = 0x0004;
            egl.eglGetConfigAttrib(display, configs[i], EGL10.EGL_RENDERABLE_TYPE, attrib);
            if ((attrib[0] & EGL_OPENGL_ES_BIT) != 0) {
                addExtensionsForConfig(egl, display, configs[i], surfaceSize, null, glExtensions);
            }
            if ((attrib[0] & EGL_OPENGL_ES2_BIT) != 0) {
                addExtensionsForConfig(egl, display, configs[i], surfaceSize, gles2, glExtensions);
            }
        }

        // Release all EGL resources.
        egl.eglTerminate(display);

        return glExtensions;
    }

    private void checkResourceConfig(Configuration config, DisplayMetrics metrics,
            ResourcesConfigurationProto resConfig) {
        final int width, height;
        if (metrics.widthPixels >= metrics.heightPixels) {
            width = metrics.widthPixels;
            height = metrics.heightPixels;
        } else {
            //noinspection SuspiciousNameCombination
            width = metrics.heightPixels;
            //noinspection SuspiciousNameCombination
            height = metrics.widthPixels;
        }

        assertEquals("Expected SDK version does not match",
                Build.VERSION.RESOURCES_SDK_INT, resConfig.sdkVersion);
        assertEquals("Expected screen width px does not match",
                width, resConfig.screenWidthPx);
        assertEquals("Expected screen width px does not match",
                height, resConfig.screenHeightPx);

        assertEquals("Expected font scale does not match",
                config.fontScale, resConfig.configuration.fontScale, Float.MIN_VALUE*5);
        assertEquals("Expected mcc does not match",
                config.mcc, resConfig.configuration.mcc);
        assertEquals("Expected mnc does not match",
                config.mnc, resConfig.configuration.mnc);
        LocaleList llist = config.getLocales();
        LocaleList lprotos = LocaleList.forLanguageTags(resConfig.configuration.localeList);
        assertEquals("Expected number of locales does not match", llist.size(), lprotos.size());
        for (int i = 0; i < llist.size(); i++) {
            assertEquals("Expected locale #" + i + " does not match",
                    llist.get(i).toLanguageTag(), lprotos.get(i).toLanguageTag());
        }
        assertEquals("Expected screen layout does not match",
                config.screenLayout, resConfig.configuration.screenLayout);
        assertEquals("Expected color mode does not match",
                config.colorMode, resConfig.configuration.colorMode);
        assertEquals("Expected touchscreen does not match",
                config.touchscreen, resConfig.configuration.touchscreen);
        assertEquals("Expected keyboard does not match",
                config.keyboard, resConfig.configuration.keyboard);
        assertEquals("Expected keyboard hidden does not match",
                config.keyboardHidden, resConfig.configuration.keyboardHidden);
        assertEquals("Expected hard keyboard hidden does not match",
                config.hardKeyboardHidden, resConfig.configuration.hardKeyboardHidden);
        assertEquals("Expected navigation does not match",
                config.navigation, resConfig.configuration.navigation);
        assertEquals("Expected navigation hidden does not match",
                config.navigationHidden, resConfig.configuration.navigationHidden);
        assertEquals("Expected orientation does not match",
                config.orientation, resConfig.configuration.orientation);
        assertEquals("Expected UI mode does not match",
                config.uiMode, resConfig.configuration.uiMode);
        assertEquals("Expected screen width dp does not match",
                config.screenWidthDp, resConfig.configuration.screenWidthDp);
        assertEquals("Expected screen hight dp does not match",
                config.screenHeightDp, resConfig.configuration.screenHeightDp);
        assertEquals("Expected smallest screen width dp does not match",
                config.smallestScreenWidthDp, resConfig.configuration.smallestScreenWidthDp);
        assertEquals("Expected density dpi does not match",
                config.densityDpi, resConfig.configuration.densityDpi);
        // XXX not comparing windowConfiguration, since by definition this is contextual.
    }

    private void checkDeviceConfig(DisplayMetrics displayMetrics,
            DeviceConfigurationProto deviceConfig) {
        assertEquals("Expected stable screen width does not match",
                displayMetrics.widthPixels, deviceConfig.stableScreenWidthPx);
        assertEquals("Expected stable screen height does not match",
                displayMetrics.heightPixels, deviceConfig.stableScreenHeightPx);
        assertEquals("Expected stable screen density does not match",
                DisplayMetrics.DENSITY_DEVICE_STABLE, deviceConfig.stableDensityDpi);

        assertEquals("Expected total RAM does not match",
                mAm.getTotalRam(), deviceConfig.totalRam);
        assertEquals("Expected low RAM does not match",
                mAm.isLowRamDevice(), deviceConfig.lowRam);
        assertEquals("Expected max cores does not match",
                Runtime.getRuntime().availableProcessors(), deviceConfig.maxCores);
        KeyguardManager kgm = mContext.getSystemService(KeyguardManager.class);
        assertEquals("Expected has secure screen lock does not match",
                kgm.isDeviceSecure(), deviceConfig.hasSecureScreenLock);

        ConfigurationInfo configInfo = mAm.getDeviceConfigurationInfo();
        if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) {
            assertEquals("Expected opengl version does not match",
                    configInfo.reqGlEsVersion, deviceConfig.openglVersion);
        }

        Set<String> glExtensionsSet = getGlExtensionsFromDriver();
        String[] glExtensions = new String[glExtensionsSet.size()];
        glExtensions = glExtensionsSet.toArray(glExtensions);
        Arrays.sort(glExtensions);
        assertArrayEquals("Expected opengl extensions does not match",
                glExtensions, deviceConfig.openglExtensions);

        List<SharedLibraryInfo> slibs = mPm.getSharedLibraries(0);
        Collections.sort(slibs, Comparator.comparing(SharedLibraryInfo::getName));
        String[] slibNames = new String[slibs.size()];
        for (int i = 0; i < slibs.size(); i++) {
            slibNames[i] = slibs.get(i).getName();
        }
        assertArrayEquals("Expected shared libraries does not match",
                slibNames, deviceConfig.sharedLibraries);

        FeatureInfo[] features = mPm.getSystemAvailableFeatures();
        Arrays.sort(features, (o1, o2) -> {
            if (o1.name == o2.name) return 0;
            if (o1.name == null) return -1;
            if (o2.name == null) return 1;
            return o1.name.compareTo(o2.name);
        });

        int size = 0;
        for (int i = 0; i < features.length; i++) {
            if (features[i].name != null) {
                size++;
            }
        }
        String[] featureNames = new String[size];
        for (int i = 0, j = 0; i < features.length; i++) {
            if (features[i].name != null) {
                featureNames[j] = features[i].name;
                j++;
            }
        }
        assertArrayEquals("Expected features does not match",
                featureNames, deviceConfig.features);
    }

    @Test
    public void testDeviceConfig() {
        byte[] dump = executeShellCommand("cmd activity get-config --proto --device");
        GlobalConfigurationProto globalConfig;
        try {
            globalConfig = GlobalConfigurationProto.parseFrom(dump);
        } catch (InvalidProtocolBufferNanoException ex) {
            throw new RuntimeException("Failed to parse get-config:\n"
                    + new String(dump, StandardCharsets.UTF_8), ex);
        }

        Configuration config = mContext.getResources().getConfiguration();
        DisplayManager dm = mContext.getSystemService(DisplayManager.class);
        Display display = dm.getDisplay(Display.DEFAULT_DISPLAY);
        DisplayMetrics metrics = new DisplayMetrics();
        display.getMetrics(metrics);

        checkResourceConfig(config, metrics, globalConfig.resources);
        checkDeviceConfig(metrics, globalConfig.device);
    }

    @Test
    public void testDeviceConfigWithSecondaryDisplay() throws Exception {
        VirtualDisplayHelper vd = new VirtualDisplayHelper();
        final int displayId = vd.setPublicDisplay(true).createAndWaitForDisplay();

        DisplayManager dm = mContext.getSystemService(DisplayManager.class);
        Display display = dm.getDisplay(displayId);
        DisplayMetrics metrics = new DisplayMetrics();
        display.getMetrics(metrics);
        String cmd = "cmd activity get-config --proto --device --display " + displayId;
        byte[] dump = executeShellCommand(cmd);

        GlobalConfigurationProto globalConfig;
        globalConfig = GlobalConfigurationProto.parseFrom(dump);
        Configuration config = mContext.getResources().getConfiguration();

        checkResourceConfig(config, metrics, globalConfig.resources);
        checkDeviceConfig(metrics, globalConfig.device);

        vd.releaseDisplay();
    }
}
