build/mobile/robocop/FennecNativeDriver.java.in
author Gregory Szorc <gps@mozilla.com>
Wed, 28 Jan 2015 13:37:00 -0800
branchGECKO160_2012091123_RELBRANCH
changeset 243304 de801bc4b960fcd6787cb12a0c38d8b5153e5630
parent 98690 d3c66ed8fea9c8eabe7a55db954cd80f1fa667b6
child 107168 e10b948a6e63c87f88da6951c7e6d47a315f0a7b
permissions -rw-r--r--
Close old release branch GECKO160_2012091123_RELBRANCH

#filter substitution
/* 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 @ANDROID_PACKAGE_NAME@;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.IntBuffer;
import java.util.HashMap;
import java.util.List;

import java.lang.Class;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.Long;

import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.view.View;
import android.util.Log;

import org.json.*;

import com.jayway.android.robotium.solo.Solo;

public class FennecNativeDriver implements Driver {
    private static final int FRAME_TIME_THRESHOLD = 25;     // allow 25ms per frame (40fps)

    // Map of IDs to element names.
    private HashMap mLocators = null;
    private Activity mActivity;
    private Solo mSolo;

    private static String mLogFile = null;
    private static LogLevel mLogLevel = LogLevel.INFO;

    // Objects for reflexive access of fennec classes.
    private ClassLoader mClassLoader;
    private Class mGel;
    private Class mGe;
    private Class mGas;
    private Method mRegisterGEL;
    private Method mUnregisterGEL;
    private Method mSendGE;
    private Method _startFrameRecording;
    private Method _stopFrameRecording;
    private Method _startCheckerboardRecording;
    private Method _stopCheckerboardRecording;
    private Method _getPixels;

    public enum LogLevel {
        DEBUG(1),
        INFO(2),
        WARN(3),
        ERROR(4);

        private int mValue;
        LogLevel(int value) {
            mValue = value;
        }
        public boolean isEnabled(LogLevel configuredLevel) {
            return mValue >= configuredLevel.getValue();
        }
        private int getValue() {
            return mValue;
        }
    }

    public FennecNativeDriver(Activity activity, Solo robocop) {
        mActivity = activity;
        mSolo = robocop;

        // Set up table of fennec_ids.
        mLocators = convertTextToTable(getFile("/mnt/sdcard/fennec_ids.txt"));

        // Set up reflexive access of java classes and methods.
        try {
            mClassLoader = activity.getClassLoader();
            mGel = mClassLoader.loadClass("org.mozilla.gecko.GeckoEventListener");
            mGe = mClassLoader.loadClass("org.mozilla.gecko.GeckoEvent");
            mGas = mClassLoader.loadClass("org.mozilla.gecko.GeckoAppShell");
            Class [] parameters = new Class[2];
            parameters[0] = String.class;
            parameters[1] = mGel;
            mRegisterGEL = mGas.getMethod("registerGeckoEventListener", parameters);
            mUnregisterGEL = mGas.getMethod("unregisterGeckoEventListener", parameters);
            parameters = new Class[1];
            parameters[0] = mGe;
            mSendGE = mGas.getMethod("sendEventToGecko", parameters);

            Class gfx = mClassLoader.loadClass("org.mozilla.gecko.gfx.PanningPerfAPI");
            _startFrameRecording = gfx.getDeclaredMethod("startFrameTimeRecording");
            _stopFrameRecording = gfx.getDeclaredMethod("stopFrameTimeRecording");
            _startCheckerboardRecording = gfx.getDeclaredMethod("startCheckerboardRecording");
            _stopCheckerboardRecording = gfx.getDeclaredMethod("stopCheckerboardRecording");

            Class layerView = mClassLoader.loadClass("org.mozilla.gecko.gfx.LayerView");
            _getPixels = layerView.getDeclaredMethod("getPixels");
        } catch (ClassNotFoundException e) {
            log(LogLevel.ERROR, e);
        } catch (SecurityException e) {
            log(LogLevel.ERROR, e);
        } catch (NoSuchMethodException e) {
            log(LogLevel.ERROR, e);
        } catch (IllegalArgumentException e) {
            log(LogLevel.ERROR, e);
        }
    }

    //Information on the location of the Gecko Frame.
    private boolean mGeckoInfo = false;
    private int mGeckoTop = 100;
    private int mGeckoLeft = 0;
    private int mGeckoHeight= 700;
    private int mGeckoWidth = 1024;

    private void getGeckoInfo() {
        View geckoLayout = mActivity.findViewById(Integer.decode((String)mLocators.get("gecko_layout")));
        if (geckoLayout != null) {
            int[] pos = new int[2];
            geckoLayout.getLocationOnScreen(pos);
            mGeckoTop = pos[1];
            mGeckoLeft = pos[0];
            mGeckoWidth = geckoLayout.getWidth();
            mGeckoHeight = geckoLayout.getHeight();
            mGeckoInfo = true;
        } else {
            throw new RoboCopException("Unable to find view gecko_layout");
        }
    }

    public int getGeckoTop() {
        if (!mGeckoInfo) {
            getGeckoInfo();
        }
        return mGeckoTop;
    }

    public int getGeckoLeft() {
        if (!mGeckoInfo) {
            getGeckoInfo();
        }
        return mGeckoLeft;
    }

    public int getGeckoHeight() {
        if (!mGeckoInfo) {
            getGeckoInfo();
        }
        return mGeckoHeight;
    }

    public int getGeckoWidth() {
        if (!mGeckoInfo) {
            getGeckoInfo();
        }
        return mGeckoWidth;
    }

    /** Find the named element in the list of known Fennec views. 
     *  @return An Element representing the view, or null if the view is not found.
     */
    public Element findElement(Activity activity, String name) {
        if (name == null) {
            FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR,
                "Can not findElements when passed a null");
            return null;
        }
        if (mLocators.containsKey(name)) {
            return new FennecNativeElement(Integer.decode((String)mLocators.get(name)), activity, mSolo);
        }
        FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR,
            "findElement: Element '"+name+"' does not exist in the list");
        return null;
    }

    public void startFrameRecording() {
        try {
            Object [] params = null;
            _startFrameRecording.invoke(null, params);
        } catch (IllegalAccessException e) {
            log(LogLevel.ERROR, e);
        } catch (InvocationTargetException e) {
            log(LogLevel.ERROR, e);
        }
    }

    public int stopFrameRecording() {
        Class [] parameters = new Class[1];
        parameters[0] = null;

        try {
            Object [] params = null;
            List<Long> frames = (List<Long>)_stopFrameRecording.invoke(null, params);
            int badness = 0;
            for (int i = 1; i < frames.size(); i++) {
                long frameTime = frames.get(i) - frames.get(i - 1);
                int delay = (int)(frameTime - FRAME_TIME_THRESHOLD);
                // for each frame we miss, add the square of the delay. This
                // makes large delays much worse than small delays.
                if (delay > 0) {
                    badness += delay * delay;
                }
            }
            // Don't do any averaging of the numbers because really we want to
            // know how bad the jank was at its worst
            return badness;
        } catch (IllegalAccessException e) {
            log(LogLevel.ERROR, e);
        } catch (InvocationTargetException e) {
            log(LogLevel.ERROR, e);
        }

        // higher values are worse, and the test failing is the worst!
        return Integer.MAX_VALUE;
    }

    public void startCheckerboardRecording() {
        try {
            Object [] params = null;
            _startCheckerboardRecording.invoke(null, params);
        } catch (IllegalAccessException e) {
            log(LogLevel.ERROR, e);
        } catch (InvocationTargetException e) {
            log(LogLevel.ERROR, e);
        }
    }

    public float stopCheckerboardRecording() {
        Class [] parameters = new Class[1];
        parameters[0] = null;

        try {
            Object [] params = null;
            List<Float> checkerboard = (List<Float>)_stopCheckerboardRecording.invoke(null, params);
            float total = 0;
            for (float val : checkerboard) {
                total += val;
            }
            return total * 100.0f;
        } catch (IllegalAccessException e) {
            log(LogLevel.ERROR, e);
        } catch (InvocationTargetException e) {
            log(LogLevel.ERROR, e);
        }

        return 0.0f;
    }

    private View getSurfaceView() {
        try {
            Class c = Class.forName("org.mozilla.gecko.gfx.LayerView");
            for (View v : mSolo.getCurrentViews()) {
                if (c.isInstance(v)) {
                    return v;
                }
            }
        } catch (ClassNotFoundException e) {
            log(LogLevel.ERROR, e);
        }
        return null;
    }

    public PaintedSurface getPaintedSurface() {
        View view = getSurfaceView();
        if (view == null) {
            return null;
        }
        IntBuffer pixelBuffer;
        try {
            pixelBuffer = (IntBuffer)_getPixels.invoke(view);
        } catch (Exception e) {
            log(LogLevel.ERROR, e);
            return null;
        }

        // now we need to (1) flip the image, because GL likes to do things up-side-down,
        // and (2) rearrange the bits from AGBR-8888 to ARGB-8888.
        int w = view.getWidth();
        int h = view.getHeight();
        pixelBuffer.position(0);
        String mapFile = "/mnt/sdcard/pixels.map";

        FileOutputStream fos = null;
        BufferedOutputStream bos = null;
        DataOutputStream dos = null;
        try {
            fos = new FileOutputStream(mapFile);
            bos = new BufferedOutputStream(fos);
            dos = new DataOutputStream(bos);

            for (int y = h - 1; y >= 0; y--) {
                for (int x = 0; x < w; x++) {
                    int agbr = pixelBuffer.get();
                    dos.writeInt((agbr & 0xFF00FF00) | ((agbr >> 16) & 0x000000FF) | ((agbr << 16) & 0x00FF0000));
                }
            }
        } catch (IOException e) {
            throw new RoboCopException("exception with pixel writer on file: " + mapFile);
        } finally {
            try {
                if (dos != null) {
                    dos.flush();
                    dos.close();
                }
                // closing dos automatically closes bos
                if (fos != null) {
                    fos.flush();
                    fos.close();
                }
            } catch (IOException e) {
                log(LogLevel.ERROR, e);
                throw new RoboCopException("exception closing pixel writer on file: " + mapFile);
            }
        }
        return new PaintedSurface(mapFile, w, h);
    }

    public int mHeight=0;
    public int mScrollHeight=0;
    public int mPageHeight=10;

    class scrollHandler implements InvocationHandler {
        public scrollHandler(){};
        public Object invoke(Object proxy, Method method, Object[] args) {
            try {
                // Disect the JSON object into the appropriate variables 
                JSONObject jo = ((JSONObject)args[1]);
                mScrollHeight = jo.getInt("y");
                mHeight = jo.getInt("cheight");
                // We don't want a height of 0. That means it's a bad response.
                if (mHeight > 0) {
                    mPageHeight = jo.getInt("height");
                }

            } catch( Throwable e) {
                FennecNativeDriver.log(FennecNativeDriver.LogLevel.WARN, 
                    "WARNING: ScrollReceived, but read wrong!");
            }
            return null;
        }
    }

    public int getScrollHeight() {
        return mScrollHeight;
    }
    public int getPageHeight() {
        return mPageHeight;
    }
    public int getHeight() {
        return mHeight;
    }

    public void setupScrollHandling() {
        //Setup scrollHandler to catch "robocop:scroll" events. 
        try {
            Class [] interfaces = new Class[1];
            interfaces[0] = mGel;
            Object[] finalParams = new Object[2];
            finalParams[0] = "robocop:scroll";
            finalParams[1] = Proxy.newProxyInstance(mClassLoader, interfaces, new scrollHandler());
            mRegisterGEL.invoke(null, finalParams);
        } catch (IllegalAccessException e) {
            log(LogLevel.ERROR, e);
        } catch (InvocationTargetException e) {
            log(LogLevel.ERROR, e);
        }

    }

    /**
     *  Takes a filename, loads the file, and returns a string version of the entire file.
     */
    public static String getFile(String filename)
    {
        StringBuilder text = new StringBuilder();

        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(filename));
            String line;

            while ((line = br.readLine()) != null) {
                text.append(line);
                text.append('\n');
            }
        } catch (IOException e) {
            log(LogLevel.ERROR, e);
        } finally {
            try {
                br.close();
            } catch (IOException e) {
            }
        }
        return text.toString();    
    }

    /**
     *  Takes a string of "key=value" pairs split by \n and creates a hash table.
     */
    public static HashMap convertTextToTable(String data)
    {
        HashMap retVal = new HashMap();

        String[] lines = data.split("\n");
        for (int i = 0; i < lines.length; i++) {
            String[] parts = lines[i].split("=");
            retVal.put(parts[0].trim(), parts[1].trim());
        }
        return retVal;
    }

    /** 
     *  Set the filename used for logging. If the file already exists, delete it
     *  as a safe-guard against accidentally appending to an old log file.
     */
    public static void setLogFile(String filename) {
        mLogFile = filename;
        File file = new File(mLogFile);
        if (file.exists()) {
            file.delete();
        }
    }

    public static void setLogLevel(LogLevel level) {
        mLogLevel = level;
    }

    public static void log(LogLevel level, String message) {
        log(level, message, null);
    }

    public static void log(LogLevel level, Throwable t) {
        log(level, null, t);
    }

    public static void log(LogLevel level, String message, Throwable t) {
        if (mLogFile == null) {
            assert(false);
        }

        if (level.isEnabled(mLogLevel)) {
            PrintWriter pw = null;
            try {
                pw = new PrintWriter(new FileWriter(mLogFile, true));
                if (message != null) {
                    pw.println(message);
                }
                if (t != null) {
                    t.printStackTrace(pw);
                }
            } catch (IOException ioe) {
                Log.e("Robocop", "exception with file writer on: " + mLogFile);
            } finally {
                pw.close();
            }
            // PrintWriter doesn't throw IOE but sets an error flag instead,
            // so check for that
            if (pw.checkError()) {
                Log.e("Robocop", "exception with file writer on: " + mLogFile);
            }
        }

        if (level == LogLevel.INFO) {
            Log.i("Robocop", message, t);
        } else if (level == LogLevel.DEBUG) {
            Log.d("Robocop", message, t);
        } else if (level == LogLevel.WARN) {
            Log.w("Robocop", message, t);
        } else if (level == LogLevel.ERROR) {
            Log.e("Robocop", message, t);
        }
    }
}