Add a test case to measure the power usage of loading a web page
diff --git a/tests/BrowserPowerTest/Android.mk b/tests/BrowserPowerTest/Android.mk
new file mode 100644
index 0000000..f2c07b3
--- /dev/null
+++ b/tests/BrowserPowerTest/Android.mk
@@ -0,0 +1,30 @@
+# Copyright 2008, 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.
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := BrowserPowerTests
+
+#LOCAL_INSTRUMENTATION_FOR := browserpowertest
+
+include $(BUILD_PACKAGE)
diff --git a/tests/BrowserPowerTest/AndroidManifest.xml b/tests/BrowserPowerTest/AndroidManifest.xml
new file mode 100644
index 0000000..43eeaad
--- /dev/null
+++ b/tests/BrowserPowerTest/AndroidManifest.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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 name must be unique so suffix with "tests" so package loader doesn't ignore us -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.browserpowertest">
+
+    <!-- We add an application tag here just so that we can indicate that
+         this package needs to link against the android.test library,
+         which is needed when building test cases. -->
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <activity android:name="PowerTestActivity" android:label="Power">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.TEST" />
+            </intent-filter>
+        </activity>
+    </application>
+    <!--
+    This declares that this app uses the instrumentation test runner targeting
+    the package of browserpowertest. To run the tests use the command:
+    "adb shell am instrument -w com.android.browserpowertest/.PowerTestRunner"
+    -->
+    <instrumentation android:name=".PowerTestRunner"
+        android:targetPackage="com.android.browserpowertest"
+        android:label="Test runner for Browser Power Tests."
+    />
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.WRITE_SDCARD" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+</manifest>
diff --git a/tests/BrowserPowerTest/src/com/android/browserpowertest/PowerMeasurement.java b/tests/BrowserPowerTest/src/com/android/browserpowertest/PowerMeasurement.java
new file mode 100644
index 0000000..74ac865f
--- /dev/null
+++ b/tests/BrowserPowerTest/src/com/android/browserpowertest/PowerMeasurement.java
@@ -0,0 +1,51 @@
+package com.android.browserpowertest;
+
+import android.content.Intent;
+import android.app.Instrumentation;
+import android.os.Handler;
+import android.os.Message;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+import junit.framework.*;
+
+public class PowerMeasurement extends ActivityInstrumentationTestCase2<PowerTestActivity> {
+
+    private static final String LOGTAG = "PowerMeasurement";
+    private static final String PKG_NAME = "com.android.browserpowertest";
+    private static final String TESTING_URL = "http://www.espn.com";
+    private static final int TIME_OUT = 2 * 60 * 1000;
+    private static final int DELAY = 0;
+
+    public PowerMeasurement() {
+        super(PKG_NAME, PowerTestActivity.class);
+    }
+
+    public void testPageLoad() throws Throwable {
+        Instrumentation mInst = getInstrumentation();
+        PowerTestActivity act = getActivity();
+
+        Intent intent = new Intent(mInst.getContext(), PowerTestActivity.class);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        long start = System.currentTimeMillis();
+        PowerTestActivity activity = (PowerTestActivity)mInst.startActivitySync(
+                intent);
+        activity.reset();
+        //send a message with the new URL
+        Handler handler = activity.getHandler();
+        Message msg = handler.obtainMessage(
+                PowerTestActivity.MSG_NAVIGATE, TIME_OUT, DELAY);
+        msg.getData().putString(PowerTestActivity.MSG_NAV_URL, TESTING_URL);
+        msg.getData().putBoolean(PowerTestActivity.MSG_NAV_LOGTIME, true);
+
+        handler.sendMessage(msg);
+        boolean timeoutFlag = activity.waitUntilDone();
+        long end = System.currentTimeMillis();
+        assertFalse(TESTING_URL + " failed to load", timeoutFlag);
+        boolean pageErrorFlag = activity.getPageError();
+        assertFalse(TESTING_URL + " is not available, either network is down or the server is down",
+                pageErrorFlag);
+        Log.v(LOGTAG, "Page is loaded in " + activity.getPageLoadTime() + " ms.");
+
+        activity.finish();
+    }
+}
diff --git a/tests/BrowserPowerTest/src/com/android/browserpowertest/PowerTestActivity.java b/tests/BrowserPowerTest/src/com/android/browserpowertest/PowerTestActivity.java
new file mode 100644
index 0000000..77e390b
--- /dev/null
+++ b/tests/BrowserPowerTest/src/com/android/browserpowertest/PowerTestActivity.java
@@ -0,0 +1,253 @@
+package com.android.browserpowertest;
+
+import android.app.Activity;
+import android.app.ActivityThread;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.ViewGroup;
+import android.webkit.WebChromeClient;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.webkit.WebSettings.LayoutAlgorithm;
+import android.widget.LinearLayout;
+import android.widget.LinearLayout.LayoutParams;
+
+public class PowerTestActivity extends Activity {
+
+    public static final String LOGTAG = "PowerTestActivity";
+    public static final String PARAM_URL = "URL";
+    public static final String PARAM_TIMEOUT = "Timeout";
+    public static final int RESULT_TIMEOUT = 0xDEAD;
+    public static final int MSG_TIMEOUT = 0xC001;
+    public static final int MSG_NAVIGATE = 0xC002;
+    public static final String MSG_NAV_URL = "url";
+    public static final String MSG_NAV_LOGTIME = "logtime";
+
+    private WebView webView;
+    private SimpleWebViewClient webViewClient;
+    private SimpleChromeClient chromeClient;
+    private Handler handler;
+    private boolean timeoutFlag;
+    private boolean logTime;
+    private boolean pageDone;
+    private Object pageDoneLock;
+    private int pageStartCount;
+    private int manualDelay;
+    private long startTime;
+    private long pageLoadTime;
+    private PageDoneRunner pageDoneRunner = new PageDoneRunner();
+
+    public PowerTestActivity() {
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Log.v(LOGTAG, "onCreate, inst=" + Integer.toHexString(hashCode()));
+
+        LinearLayout contentView = new LinearLayout(this);
+        contentView.setOrientation(LinearLayout.VERTICAL);
+        setContentView(contentView);
+        setTitle("Idle");
+
+        webView = new WebView(this);
+        webView.getSettings().setJavaScriptEnabled(true);
+        webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(false);
+        webView.getSettings().setLayoutAlgorithm(LayoutAlgorithm.NORMAL);
+
+        webViewClient = new SimpleWebViewClient();
+        chromeClient = new SimpleChromeClient();
+        webView.setWebViewClient(webViewClient);
+        webView.setWebChromeClient(chromeClient);
+
+        contentView.addView(webView, new LayoutParams(
+                ViewGroup.LayoutParams.FILL_PARENT,
+                ViewGroup.LayoutParams.FILL_PARENT, 0.0f));
+
+        handler = new Handler() {
+            @Override
+            public void handleMessage(Message msg) {
+                switch (msg.what) {
+                    case MSG_TIMEOUT:
+                        handleTimeout();
+                        return;
+                    case MSG_NAVIGATE:
+                        manualDelay = msg.arg2;
+                        navigate(msg.getData().getString(MSG_NAV_URL), msg.arg1);
+                        logTime = msg.getData().getBoolean(MSG_NAV_LOGTIME);
+                        return;
+                }
+            }
+        };
+
+        pageDoneLock = new Object();
+    }
+
+    public void reset() {
+        synchronized (pageDoneLock) {
+            pageDone = false;
+        }
+        timeoutFlag = false;
+        pageStartCount = 0;
+        chromeClient.resetJsTimeout();
+    }
+
+    private void navigate(String url, int timeout) {
+        if(url == null) {
+            Log.v(LOGTAG, "URL is null, cancelling...");
+            finish();
+        }
+        webView.stopLoading();
+        if(logTime) {
+            webView.clearCache(true);
+        }
+        startTime = System.currentTimeMillis();
+        Log.v(LOGTAG, "Navigating to URL: " + url);
+        webView.loadUrl(url);
+
+        if(timeout != 0) {
+            //set a timer with specified timeout (in ms)
+            handler.sendMessageDelayed(handler.obtainMessage(MSG_TIMEOUT),
+                    timeout);
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        Log.v(LOGTAG, "onDestroy, inst=" + Integer.toHexString(hashCode()));
+        webView.clearCache(true);
+        webView.destroy();
+    }
+
+    private boolean isPageDone() {
+        synchronized (pageDoneLock) {
+            return pageDone;
+        }
+    }
+
+    private void setPageDone(boolean pageDone) {
+        synchronized (pageDoneLock) {
+            this.pageDone = pageDone;
+            pageDoneLock.notifyAll();
+        }
+    }
+
+    private void handleTimeout() {
+        int progress = webView.getProgress();
+        webView.stopLoading();
+        Log.v(LOGTAG, "Page timeout triggered, progress = " + progress);
+        timeoutFlag = true;
+        handler.postDelayed(pageDoneRunner, manualDelay);
+    }
+
+    public boolean waitUntilDone() {
+        validateNotAppThread();
+        synchronized (pageDoneLock) {
+            while(!isPageDone()) {
+                try {
+                    pageDoneLock.wait();
+                } catch (InterruptedException ie) {
+                    //no-op
+                }
+            }
+        }
+        return timeoutFlag;
+    }
+
+    public Handler getHandler() {
+        return handler;
+    }
+
+    private final void validateNotAppThread() {
+        if (ActivityThread.currentActivityThread() != null) {
+            throw new RuntimeException(
+                "This method can not be called from the main application thread");
+        }
+    }
+
+    public long getPageLoadTime() {
+        return pageLoadTime;
+    }
+
+    public boolean getPageError() {
+        return webViewClient.getPageErrorFlag();
+    }
+
+    class SimpleWebViewClient extends WebViewClient {
+
+        private boolean pageErrorFlag = false;
+
+        @Override
+        public void onReceivedError(WebView view, int errorCode, String description,
+                String failingUrl) {
+            pageErrorFlag = true;
+            Log.v(LOGTAG, "WebCore error: code=" + errorCode
+                    + ", description=" + description
+                    + ", url=" + failingUrl);
+        }
+
+        @Override
+        public void onPageStarted(WebView view, String url, Bitmap favicon) {
+            pageStartCount++;
+            Log.v(LOGTAG, "onPageStarted: " + url);
+        }
+
+        @Override
+        public void onPageFinished(WebView view, String url) {
+            Log.v(LOGTAG, "onPageFinished: " + url);
+            // let handleTimeout take care of finishing the page
+            if(!timeoutFlag)
+                handler.postDelayed(new WebViewStatusChecker(), 500);
+        }
+
+        // return true if the URL is not available or the page is down
+        public boolean getPageErrorFlag() {
+            return pageErrorFlag;
+        }
+    }
+
+    class SimpleChromeClient extends WebChromeClient {
+
+        private int timeoutCounter = 0;
+
+        public void resetJsTimeout() {
+            timeoutCounter = 0;
+        }
+
+        @Override
+        public void onReceivedTitle(WebView view, String title) {
+            PowerTestActivity.this.setTitle(title);
+        }
+    }
+
+    class WebViewStatusChecker implements Runnable {
+
+        private int initialStartCount;
+
+        public WebViewStatusChecker() {
+            initialStartCount = pageStartCount;
+        }
+
+        public void run() {
+            if (initialStartCount == pageStartCount && !isPageDone()) {
+                handler.removeMessages(MSG_TIMEOUT);
+                webView.stopLoading();
+                handler.postDelayed(pageDoneRunner, manualDelay);
+            }
+        }
+    }
+
+    class PageDoneRunner implements Runnable {
+
+        public void run() {
+            Log.v(LOGTAG, "Finishing URL: " + webView.getUrl());
+            pageLoadTime = System.currentTimeMillis() - startTime;
+            setPageDone(true);
+        }
+    }
+}
diff --git a/tests/BrowserPowerTest/src/com/android/browserpowertest/PowerTestRunner.java b/tests/BrowserPowerTest/src/com/android/browserpowertest/PowerTestRunner.java
new file mode 100644
index 0000000..4857209
--- /dev/null
+++ b/tests/BrowserPowerTest/src/com/android/browserpowertest/PowerTestRunner.java
@@ -0,0 +1,31 @@
+package com.android.browserpowertest;
+
+import android.test.InstrumentationTestRunner;
+import android.test.InstrumentationTestSuite;
+
+import junit.framework.TestSuite;
+
+
+/**
+ * Instrumentation Test Runner for all browser power tests.
+ *
+ * Running power tests:
+ *
+ * adb shell am instrument \
+ *   -w com.android.browserpowertest/.PowerTestRunner
+ */
+
+public class PowerTestRunner extends InstrumentationTestRunner {
+    @Override
+    public TestSuite getAllTests() {
+        TestSuite suite = new InstrumentationTestSuite(this);
+        suite.addTestSuite(PowerMeasurement.class);
+        return suite;
+    }
+
+    @Override
+    public ClassLoader getLoader() {
+        return PowerTestRunner.class.getClassLoader();
+    }
+
+}