/* 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.goanna; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.nio.IntBuffer; import java.util.HashMap; import java.util.List; import java.util.Map; import org.json.JSONException; import org.json.JSONObject; import org.mozilla.goanna.gfx.LayerView; import org.mozilla.goanna.gfx.PanningPerfAPI; import org.mozilla.goanna.util.GoannaEventListener; import android.app.Activity; import android.util.Log; import android.view.View; 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) private final Activity mActivity; private final Solo mSolo; private final String mRootPath; private static String mLogFile; private static LogLevel mLogLevel = LogLevel.INFO; public enum LogLevel { DEBUG(1), INFO(2), WARN(3), ERROR(4); private final 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, String rootPath) { mActivity = activity; mSolo = robocop; mRootPath = rootPath; } //Information on the location of the Goanna Frame. private boolean mGoannaInfo = false; private int mGoannaTop = 100; private int mGoannaLeft = 0; private int mGoannaHeight= 700; private int mGoannaWidth = 1024; private void getGoannaInfo() { View goannaLayout = mActivity.findViewById(R.id.goanna_layout); if (goannaLayout != null) { int[] pos = new int[2]; goannaLayout.getLocationOnScreen(pos); mGoannaTop = pos[1]; mGoannaLeft = pos[0]; mGoannaWidth = goannaLayout.getWidth(); mGoannaHeight = goannaLayout.getHeight(); mGoannaInfo = true; } else { throw new RoboCopException("Unable to find view goanna_layout"); } } @Override public int getGoannaTop() { if (!mGoannaInfo) { getGoannaInfo(); } return mGoannaTop; } @Override public int getGoannaLeft() { if (!mGoannaInfo) { getGoannaInfo(); } return mGoannaLeft; } @Override public int getGoannaHeight() { if (!mGoannaInfo) { getGoannaInfo(); } return mGoannaHeight; } @Override public int getGoannaWidth() { if (!mGoannaInfo) { getGoannaInfo(); } return mGoannaWidth; } /** Find the element with given id. * * @return An Element representing the view, or null if the view is not found. */ @Override public Element findElement(Activity activity, int id) { return new FennecNativeElement(id, activity); } @Override public void startFrameRecording() { PanningPerfAPI.startFrameTimeRecording(); } @Override public int stopFrameRecording() { final List frames = PanningPerfAPI.stopFrameTimeRecording(); 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; } @Override public void startCheckerboardRecording() { PanningPerfAPI.startCheckerboardRecording(); } @Override public float stopCheckerboardRecording() { final List checkerboard = PanningPerfAPI.stopCheckerboardRecording(); float total = 0; for (float val : checkerboard) { total += val; } return total * 100.0f; } private LayerView getSurfaceView() { final LayerView layerView = mSolo.getView(LayerView.class, 0); if (layerView == null) { log(LogLevel.WARN, "getSurfaceView could not find LayerView"); for (final View v : mSolo.getViews()) { log(LogLevel.WARN, " View: " + v); } } return layerView; } @Override public PaintedSurface getPaintedSurface() { final LayerView view = getSurfaceView(); if (view == null) { return null; } final IntBuffer pixelBuffer = view.getPixels(); // 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 = mRootPath + "/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; @Override public int getScrollHeight() { return mScrollHeight; } @Override public int getPageHeight() { return mPageHeight; } @Override public int getHeight() { return mHeight; } @Override public void setupScrollHandling() { EventDispatcher.getInstance().registerGoannaThreadListener(new GoannaEventListener() { @Override public void handleMessage(final String event, final JSONObject message) { try { mScrollHeight = message.getInt("y"); mHeight = message.getInt("cheight"); // We don't want a height of 0. That means it's a bad response. if (mHeight > 0) { mPageHeight = message.getInt("height"); } } catch (JSONException e) { FennecNativeDriver.log(FennecNativeDriver.LogLevel.WARN, "WARNING: ScrollReceived, but message does not contain " + "expected fields: " + e); } } }, "robocop:scroll"); } /** * 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 { if (br != null) { 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 Map 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("=", 2); retVal.put(parts[0].trim(), parts[1].trim()); } return retVal; } public static void logAllStackTraces(LogLevel level) { StringBuffer sb = new StringBuffer(); sb.append("Dumping ALL the threads!\n"); Map allStacks = Thread.getAllStackTraces(); for (Thread t : allStacks.keySet()) { sb.append(t.toString()).append('\n'); for (StackTraceElement ste : allStacks.get(t)) { sb.append(ste.toString()).append('\n'); } sb.append('\n'); } log(level, sb.toString()); } /** * 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) { throw new RuntimeException("No log file specified!"); } 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 { if (pw != null) { pw.close(); } } // PrintWriter doesn't throw IOE but sets an error flag instead, // so check for that if (pw != null && 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); } } }