Merge "[Browser2] Update OWNERS"
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..e04b939
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,6 @@
+android_app {
+    name: "Browser2",
+    sdk_version: "current",
+    srcs: ["src/**/*.java"],
+    product_specific: true,
+}
diff --git a/Android.mk b/Android.mk
deleted file mode 100644
index 8f13e41..0000000
--- a/Android.mk
+++ /dev/null
@@ -1,15 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := optional
-
-LOCAL_SDK_VERSION := current
-
-LOCAL_SRC_FILES := \
-        $(call all-java-files-under, src)
-
-LOCAL_PACKAGE_NAME := Browser2
-
-LOCAL_PRODUCT_MODULE := true
-
-include $(BUILD_PACKAGE)
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 95b33ed..4376adc 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -10,7 +10,7 @@
     android:versionCode="1"
     android:versionName="1.0" >
 
-    <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="24" />
+    <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="28" />
 
     <!-- "Normal" permissions which do not require user prompt -->
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -31,35 +31,47 @@
     <application
         android:icon="@drawable/ic_launcher"
         android:label="@string/app_name"
-        android:theme="@android:style/Theme.Light" >
+        android:theme="@android:style/Theme.Light"
+        android:networkSecurityConfig="@xml/network_security_config"
+        android:debuggable="true" >
+        <meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
+            android:value="true" />
         <activity
-            android:name=".TelemetryActivity"
-            android:label="@string/title_activity_telemetry"
-            android:exported="true">
-        </activity>
-        <activity
-            android:name=".TelemetryMemoryPressureActivity"
+            android:name="org.chromium.webview_shell.TelemetryActivity"
             android:launchMode="singleTask"
             android:label="@string/title_activity_telemetry"
             android:exported="true">
         </activity>
         <activity
-            android:name=".JankActivity"
+            android:name="org.chromium.webview_shell.TelemetryMemoryPressureActivity"
+            android:launchMode="singleTask"
+            android:label="@string/title_activity_telemetry"
+            android:exported="true">
+        </activity>
+        <activity
+            android:name="org.chromium.webview_shell.JankActivity"
             android:label="@string/title_activity_jank"
             android:noHistory="true"
             android:exported="true">
         </activity>
         <activity
-            android:name=".StartupTimeActivity"
+            android:name="org.chromium.webview_shell.StartupTimeActivity"
             android:label="@string/title_activity_startup_time"
             android:noHistory="true"
             android:exported="true">
         </activity>
         <activity
-            android:name=".WebViewBrowserActivity"
+            android:name="org.chromium.webview_shell.WebViewCreateDestroyActivity"
+            android:launchMode="singleTask"
+            android:label="@string/title_activity_create_destroy"
+            android:exported="true">
+        </activity>
+        <activity
+            android:name="org.chromium.webview_shell.WebViewBrowserActivity"
             android:label="@string/title_activity_browser"
             android:exported="true"
-            android:windowSoftInputMode="adjustResize">
+            android:windowSoftInputMode="adjustResize"
+            android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|density">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
@@ -85,5 +97,38 @@
                 <data android:mimeType="application/vnd.wap.xhtml+xml"/> <!-- XHTML MP -->
             </intent-filter>
         </activity>
+        <activity
+            android:name="org.chromium.webview_shell.WebViewLayoutTestActivity"
+            android:label="@string/title_activity_layout_test"
+            android:exported="true">
+        </activity>
+        <activity
+            android:name="org.chromium.webview_shell.WebViewThreadTestActivity"
+            android:label="@string/title_activity_thread_test"
+            android:exported="true">
+        </activity>
+        <activity android:name="org.chromium.test.broker.OnDeviceInstrumentationBroker"
+            android:exported="true"/>
+
+        <activity
+            android:name="org.chromium.webview_shell.PageCyclerTestActivity"
+            android:label="@string/title_activity_page_cycler"
+            android:exported="true">
+        </activity>
+
+        <activity
+            android:name="org.chromium.webview_shell.WebViewTracingActivity"
+            android:label="@string/title_activity_telemetry"
+            android:noHistory="true"
+            android:exported="true">
+        </activity>
+
+        <activity
+            android:name="org.chromium.webview_shell.WebViewAnimationTestActivity"
+            android:noHistory="true"
+            android:exported="true">
+        </activity>
+
+        <uses-library android:name="android.test.runner" />
     </application>
 </manifest>
diff --git a/res/drawable-mdpi/breadcrumb_arrow_black.png b/res/drawable-mdpi/breadcrumb_arrow_black.png
new file mode 100644
index 0000000..7b9ff79
--- /dev/null
+++ b/res/drawable-mdpi/breadcrumb_arrow_black.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_launcher.png b/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..96a442e
--- /dev/null
+++ b/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/res/drawable-mdpi/item_more_black.png b/res/drawable-mdpi/item_more_black.png
new file mode 100644
index 0000000..b984062
--- /dev/null
+++ b/res/drawable-mdpi/item_more_black.png
Binary files differ
diff --git a/res/layout/activity_empty.xml b/res/layout/activity_empty.xml
new file mode 100644
index 0000000..868453d
--- /dev/null
+++ b/res/layout/activity_empty.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2016 The Chromium Authors. All rights reserved.
+     Use of this source code is governed by a BSD-style license that can be
+     found in the LICENSE file.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:id="@+id/emptyview">
+</RelativeLayout>
\ No newline at end of file
diff --git a/res/layout/activity_webview_animation_test.xml b/res/layout/activity_webview_animation_test.xml
new file mode 100644
index 0000000..44cf7c0
--- /dev/null
+++ b/res/layout/activity_webview_animation_test.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2019 The Chromium Authors. All rights reserved.
+     Use of this source code is governed by a BSD-style license that can be
+     found in the LICENSE file.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+        <Button
+            android:id="@+id/translate"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/translate_button" />
+        <Button
+            android:id="@+id/scale"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/scale_button" />
+        <Button
+            android:id="@+id/rotate"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/rotate_button" />
+        <CheckBox
+            android:id="@+id/use_layer"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:checked="false"
+            android:text="@string/layer_button" />
+    </LinearLayout>
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/alpha_button"
+            android:layout_gravity="center_vertical" />
+        <SeekBar
+            android:id="@+id/view_alpha"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:max="100"
+            android:progress="100"
+            android:layout_marginStart="4dp"
+            android:layout_marginEnd="8dp" />
+    </LinearLayout>
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="0dip"
+        android:layout_weight="1">
+        <WebView
+            android:id="@+id/webview"
+            android:layout_width="300dp"
+            android:layout_height="300dp"
+            android:layout_gravity="center" />
+    </FrameLayout>
+</LinearLayout>
diff --git a/res/layout/activity_webview_browser.xml b/res/layout/activity_webview_browser.xml
index 07483c5..80464d7 100644
--- a/res/layout/activity_webview_browser.xml
+++ b/res/layout/activity_webview_browser.xml
@@ -13,7 +13,7 @@
     android:gravity="center">
     <LinearLayout
         android:orientation="horizontal"
-        android:layout_width="fill_parent"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content">
         <EditText
             android:id="@+id/url_field"
@@ -22,16 +22,18 @@
             android:layout_weight="1.0"
             android:singleLine="true"
             android:inputType="textUri"
-            android:imeOptions="actionGo" />
+            android:selectAllOnFocus="true"
+            android:imeOptions="actionGo"
+            android:importantForAutofill="no" />
         <ImageButton
-            android:layout_width="wrap_content" 
-            android:layout_height="match_parent" 
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
             android:src="@drawable/breadcrumb_arrow_black"
             android:contentDescription="@string/load_url"
             android:onClick="loadUrlFromUrlBar" />
         <ImageButton
-            android:layout_width="wrap_content" 
-            android:layout_height="match_parent" 
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
             android:src="@drawable/item_more_black"
             android:contentDescription="@string/menu_about"
             android:onClick="showPopup" />
diff --git a/res/menu/main_menu.xml b/res/menu/main_menu.xml
index a22610d..14c90c2 100644
--- a/res/menu/main_menu.xml
+++ b/res/menu/main_menu.xml
@@ -6,6 +6,15 @@
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
     <item android:id="@+id/menu_reset_webview"
           android:title="@string/menu_reset_webview"/>
+    <item android:id="@+id/menu_clear_cache"
+          android:title="@string/menu_clear_cache"/>
+    <item android:id="@+id/menu_enable_tracing"
+          android:checkable="true"
+          android:title="@string/menu_enable_tracing"/>
+    <item android:id="@+id/menu_print"
+          android:title="@string/menu_print"/>
+    <item android:id="@+id/start_animation_activity"
+          android:title="@string/menu_start_animation_activity"/>
     <item android:id="@+id/menu_about"
           android:title="@string/menu_about"/>
 </menu>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 1108c76..c39328a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -10,7 +10,22 @@
     <string name="title_activity_jank">WebView Jank Tester</string>
     <string name="title_activity_startup_time">WebView Startup Time Tester</string>
     <string name="title_activity_browser">WebView Browser Tester</string>
+    <string name="title_activity_layout_test">WebView Layout Test</string>
+    <string name="title_activity_thread_test">WebView Thread Test</string>
+    <string name="title_activity_page_cycler">WebView Page Cycler Test</string>
+    <string name="title_activity_create_destroy">WebView Create Destroy</string>
     <string name="menu_reset_webview">Destroy and create new WebView</string>
+    <string name="menu_clear_cache">Clear cache</string>
+    <string name="menu_enable_tracing">Enable tracing</string>
+    <string name="menu_start_animation_activity">Animation test</string>
+    <string name="menu_print">Print</string>
     <string name="menu_about">About WebView</string>
     <string name="load_url">Load URL</string>
+
+    <!-- activity_webview_animation_test strings -->
+    <string name="alpha_button">View Alpha</string>
+    <string name="layer_button">Layer</string>
+    <string name="rotate_button">Rotate</string>
+    <string name="scale_button">Scale</string>
+    <string name="translate_button">Translate</string>
 </resources>
diff --git a/res/xml/network_security_config.xml b/res/xml/network_security_config.xml
new file mode 100644
index 0000000..4669e3b
--- /dev/null
+++ b/res/xml/network_security_config.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 The Chromium Authors. All rights reserved.
+     Use of this source code is governed by a BSD-style license that can be
+     found in the LICENSE file. -->
+
+<network-security-config>
+    <!-- Starting with Android P (API level 28), the default value of
+    isCleartextTrafficPermitted() is false. For the SystemWebViewShell
+    test browser we explicitly set cleartextTrafficPermitted here to
+    preserve functionality. (crbug.com/898190) -->
+    <base-config cleartextTrafficPermitted="true">
+        <trust-anchors>
+            <certificates src="user"/>
+            <certificates src="system"/>
+        </trust-anchors>
+    </base-config>
+</network-security-config>
\ No newline at end of file
diff --git a/src/org/chromium/webview_shell/JankActivity.java b/src/org/chromium/webview_shell/JankActivity.java
index 13390b7..a508e44 100644
--- a/src/org/chromium/webview_shell/JankActivity.java
+++ b/src/org/chromium/webview_shell/JankActivity.java
@@ -28,6 +28,7 @@
         CookieManager.setAcceptFileSchemeCookies(true);
 
         webView.setWebViewClient(new WebViewClient() {
+            @SuppressWarnings("deprecation") // because we support api level 19 and up.
             @Override
             public boolean shouldOverrideUrlLoading(WebView webView, String url) {
                 return false;
diff --git a/src/org/chromium/webview_shell/TelemetryActivity.java b/src/org/chromium/webview_shell/TelemetryActivity.java
index 11fa10b..a29e525 100644
--- a/src/org/chromium/webview_shell/TelemetryActivity.java
+++ b/src/org/chromium/webview_shell/TelemetryActivity.java
@@ -1,12 +1,14 @@
 // Copyright 2015 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
-
 package org.chromium.webview_shell;
 
 import android.app.Activity;
+import android.content.Intent;
 import android.os.Bundle;
+import android.os.Trace;
 import android.webkit.CookieManager;
+import android.webkit.WebSettings;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 
@@ -14,25 +16,58 @@
  * This activity is designed for Telemetry testing of WebView.
  */
 public class TelemetryActivity extends Activity {
+    static final String DEFAULT_START_UP_TRACE_TAG = "WebViewStartupInterval";
+    static final String DEFAULT_LOAD_URL_TRACE_TAG = "WebViewBlankUrlLoadInterval";
+    static final String DEFAULT_START_UP_AND_LOAD_URL_TRACE_TAG =
+            "WebViewStartupAndLoadBlankUrlInterval";
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         getWindow().setTitle(
                 getResources().getString(R.string.title_activity_telemetry));
-        setContentView(R.layout.activity_webview);
 
-        WebView webView = (WebView) findViewById(R.id.webview);
+        Intent intent = getIntent();
+        final String startUpTraceTag = intent.getStringExtra("WebViewStartUpTraceTag");
+        final String loadUrlTraceTag = intent.getStringExtra("WebViewLoadUrlTraceTag");
+        final String startUpAndLoadUrlTraceTag =
+                intent.getStringExtra("WebViewStartUpAndLoadUrlTraceTag");
+
+        Trace.beginSection(startUpTraceTag == null ? DEFAULT_START_UP_AND_LOAD_URL_TRACE_TAG
+                                                   : startUpAndLoadUrlTraceTag);
+        Trace.beginSection(startUpTraceTag == null ? DEFAULT_START_UP_TRACE_TAG : startUpTraceTag);
+        WebView webView = new WebView(this);
+        setContentView(webView);
+        Trace.endSection();
+
         CookieManager.setAcceptFileSchemeCookies(true);
-        webView.getSettings().setJavaScriptEnabled(true);
+        WebSettings settings = webView.getSettings();
+        settings.setJavaScriptEnabled(true);
+        settings.setUseWideViewPort(true);
+        settings.setLoadWithOverviewMode(true);
+        settings.setDomStorageEnabled(true);
+        settings.setMediaPlaybackRequiresUserGesture(false);
+        String userAgentString = intent.getStringExtra("userAgent");
+        if (userAgentString != null) {
+            settings.setUserAgentString(userAgentString);
+        }
 
         webView.setWebViewClient(new WebViewClient() {
-                @Override
-                public boolean shouldOverrideUrlLoading(WebView webView, String url) {
-                    return false;
-                }
+            @SuppressWarnings("deprecation") // because we support api level 19 and up.
+            @Override
+            public boolean shouldOverrideUrlLoading(WebView view, String url) {
+                return false;
+            }
+
+            @Override
+            public void onPageFinished(WebView view, String url) {
+                super.onPageFinished(view, url);
+                Trace.endSection();
+                Trace.endSection();
+            }
         });
 
+        Trace.beginSection(loadUrlTraceTag == null ? DEFAULT_LOAD_URL_TRACE_TAG : loadUrlTraceTag);
         webView.loadUrl("about:blank");
     }
 }
diff --git a/src/org/chromium/webview_shell/TelemetryMemoryPressureActivity.java b/src/org/chromium/webview_shell/TelemetryMemoryPressureActivity.java
index d9e6dfc..a6a9de8 100644
--- a/src/org/chromium/webview_shell/TelemetryMemoryPressureActivity.java
+++ b/src/org/chromium/webview_shell/TelemetryMemoryPressureActivity.java
@@ -30,10 +30,11 @@
         webview.getSettings().setJavaScriptEnabled(true);
 
         webview.setWebViewClient(new WebViewClient() {
-                @Override
-                public boolean shouldOverrideUrlLoading(WebView webView, String url) {
-                    return false;
-                }
+            @SuppressWarnings("deprecation") // because we support api level 19 and up.
+            @Override
+            public boolean shouldOverrideUrlLoading(WebView webView, String url) {
+                return false;
+            }
         });
 
         webview.loadUrl("about:blank");
diff --git a/src/org/chromium/webview_shell/WebViewAnimationTestActivity.java b/src/org/chromium/webview_shell/WebViewAnimationTestActivity.java
new file mode 100644
index 0000000..e67b6e5
--- /dev/null
+++ b/src/org/chromium/webview_shell/WebViewAnimationTestActivity.java
@@ -0,0 +1,157 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.webview_shell;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.WindowManager;
+import android.webkit.WebView;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+/**
+ * Activity to exercise transform animations on WebView.
+ */
+public class WebViewAnimationTestActivity extends Activity {
+    private static final String HTML = "<html>"
+            + "  <head>"
+            + "    <style type =\"text/css\">"
+            + "      .container {"
+            + "            display: grid;"
+            + "            grid-template-columns: 100px 100px 100px 100px 100px;"
+            + "            grid-template-rows: 100px 100px 100px 100px 100px;"
+            + "      }"
+            + "     .alt1 {"
+            + "       background-color: #aaffaa;"
+            + "     }"
+            + "     .alt2 {"
+            + "       background-color: #ff4545;"
+            + "     }"
+            + "    </style>"
+            + "  </head>"
+            + "  <body>"
+            + "   <div class=\"container\">"
+            + "     <div class=\"alt1\">00</div>"
+            + "     <div class=\"alt2\">01</div>"
+            + "     <div class=\"alt1\">02</div>"
+            + "     <div class=\"alt2\">03</div>"
+            + "     <div class=\"alt1\">04</div>"
+            + "     <div class=\"alt2\">05</div>"
+            + "     <div class=\"alt1\">06</div>"
+            + "     <div class=\"alt2\">07</div>"
+            + "     <div class=\"alt1\">08</div>"
+            + "     <div class=\"alt2\">09</div>"
+            + "     <div class=\"alt1\">10</div>"
+            + "     <div class=\"alt2\">11</div>"
+            + "     <div class=\"alt1\">12</div>"
+            + "     <div class=\"alt2\">13</div>"
+            + "     <div class=\"alt1\">14</div>"
+            + "     <div class=\"alt2\">15</div>"
+            + "     <div class=\"alt1\">16</div>"
+            + "     <div class=\"alt2\">17</div>"
+            + "     <div class=\"alt1\">18</div>"
+            + "     <div class=\"alt2\">19</div>"
+            + "     <div class=\"alt1\">20</div>"
+            + "     <div class=\"alt2\">21</div>"
+            + "     <div class=\"alt1\">22</div>"
+            + "     <div class=\"alt2\">23</div>"
+            + "     <div class=\"alt1\">24</div>"
+            + "   </div>"
+            + "  </body>"
+            + "</html>";
+
+    private WebView mWebView;
+    private boolean mIsWindowHardwareAccelerated;
+
+    /** Called when the activity is first created. */
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_webview_animation_test);
+        mWebView = (WebView) findViewById(R.id.webview);
+
+        mIsWindowHardwareAccelerated =
+                (getWindow().getAttributes().flags
+                        | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED)
+                != 0;
+        mWebView.setBackgroundColor(0);
+        mWebView.loadDataWithBaseURL("http://foo.bar", HTML, "text/html", null, "http://foo.bar");
+        OnClickListener onClickListner = (View v) -> {
+            switch (v.getId()) {
+                case R.id.translate:
+                    runTranslate();
+                    break;
+                case R.id.scale:
+                    runScale();
+                    break;
+                case R.id.rotate:
+                    runRotate();
+                    break;
+            }
+        };
+        findViewById(R.id.scale).setOnClickListener(onClickListner);
+        findViewById(R.id.translate).setOnClickListener(onClickListner);
+        findViewById(R.id.rotate).setOnClickListener(onClickListner);
+        ((SeekBar) findViewById(R.id.view_alpha))
+                .setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+                    @Override
+                    public void onProgressChanged(SeekBar view, int progress, boolean fromUser) {
+                        switch (view.getId()) {
+                            case R.id.view_alpha:
+                                mWebView.setAlpha(progress / 100f);
+                                break;
+                        }
+                    }
+
+                    @Override
+                    public void onStartTrackingTouch(SeekBar seekBar) {}
+
+                    @Override
+                    public void onStopTrackingTouch(SeekBar seekBar) {}
+                });
+        CheckBox checkBox = ((CheckBox) findViewById(R.id.use_layer));
+        checkBox.setOnCheckedChangeListener(
+                (CompoundButton arg0, boolean checked) -> { setWebViewLayer(checked); });
+        setWebViewLayer(checkBox.isChecked());
+    }
+
+    private void runTranslate() {
+        if (mWebView.getTranslationX() == 0f) {
+            mWebView.animate().translationX(100f).translationY(100f);
+        } else {
+            mWebView.animate().translationX(0f).translationY(0f);
+        }
+    }
+
+    private void runScale() {
+        if (mWebView.getScaleX() == 1f) {
+            mWebView.animate().scaleX(.5f).scaleY(.5f);
+        } else {
+            mWebView.animate().scaleX(1f).scaleY(1f);
+        }
+    }
+
+    private void runRotate() {
+        if (mWebView.getRotationX() == 0f) {
+            mWebView.animate().rotationX(45f).rotationY(45f).rotation(90f);
+        } else {
+            mWebView.animate().rotationX(0f).rotationY(0f).rotation(0f);
+        }
+    }
+
+    private void setWebViewLayer(boolean isOnLayer) {
+        if (isOnLayer) {
+            mWebView.setLayerType(mIsWindowHardwareAccelerated ? View.LAYER_TYPE_HARDWARE
+                                                               : View.LAYER_TYPE_SOFTWARE,
+                    null);
+        } else {
+            mWebView.setLayerType(View.LAYER_TYPE_NONE, null);
+        }
+    }
+}
diff --git a/src/org/chromium/webview_shell/WebViewBrowserActivity.java b/src/org/chromium/webview_shell/WebViewBrowserActivity.java
index 1da563e..0a7b637 100644
--- a/src/org/chromium/webview_shell/WebViewBrowserActivity.java
+++ b/src/org/chromium/webview_shell/WebViewBrowserActivity.java
@@ -5,6 +5,8 @@
 package org.chromium.webview_shell;
 
 import android.Manifest;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.ActivityNotFoundException;
@@ -18,39 +20,47 @@
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.StrictMode;
+import android.print.PrintAttributes;
+import android.print.PrintDocumentAdapter;
+import android.print.PrintManager;
 import android.provider.Browser;
 import android.util.Log;
 import android.util.SparseArray;
-
+import android.view.Gravity;
 import android.view.KeyEvent;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnKeyListener;
 import android.view.ViewGroup;
 import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
 import android.view.inputmethod.InputMethodManager;
-
 import android.webkit.GeolocationPermissions;
 import android.webkit.PermissionRequest;
+import android.webkit.TracingConfig;
+import android.webkit.TracingController;
 import android.webkit.WebChromeClient;
-import android.webkit.WebResourceRequest;
 import android.webkit.WebSettings;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
-
 import android.widget.EditText;
+import android.widget.FrameLayout;
 import android.widget.PopupMenu;
 import android.widget.TextView;
+import android.widget.Toast;
 
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
-
-import java.net.URI;
-import java.net.URISyntaxException;
-
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.Executors;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -69,6 +79,12 @@
     // WebKit permissions with no corresponding Android permission can always be granted
     private static final String NO_ANDROID_PERMISSION = "NO_ANDROID_PERMISSION";
 
+    // TODO(timav): Remove these variables after http://crbug.com/626202 is fixed.
+    // The Bundle key for WebView serialized state
+    private static final String SAVE_RESTORE_STATE_KEY = "WEBVIEW_CHROMIUM_STATE";
+    // Maximal size of this state.
+    private static final int MAX_STATE_LENGTH = 300 * 1024;
+
     // Map from WebKit permissions to Android permissions
     private static final HashMap<String, String> sPermissions;
     static {
@@ -88,15 +104,18 @@
 
     private EditText mUrlBar;
     private WebView mWebView;
+    private View mFullscreenView;
     private String mWebViewVersion;
+    private boolean mEnableTracing;
 
     // Each time we make a request, store it here with an int key. onRequestPermissionsResult will
     // look up the request in order to grant the approprate permissions.
     private SparseArray<PermissionRequest> mPendingRequests = new SparseArray<PermissionRequest>();
-    private int mNextRequestKey = 0;
+    private int mNextRequestKey;
 
     // Work around our wonky API by wrapping a geo permission prompt inside a regular
     // PermissionRequest.
+    @SuppressLint("NewApi") // GeoPermissionRequest class requires API level 21.
     private static class GeoPermissionRequest extends PermissionRequest {
         private String mOrigin;
         private GeolocationPermissions.Callback mCallback;
@@ -106,20 +125,24 @@
             mCallback = callback;
         }
 
+        @Override
         public Uri getOrigin() {
             return Uri.parse(mOrigin);
         }
 
+        @Override
         public String[] getResources() {
             return new String[] { WebViewBrowserActivity.RESOURCE_GEO };
         }
 
+        @Override
         public void grant(String[] resources) {
             assert resources.length == 1;
             assert WebViewBrowserActivity.RESOURCE_GEO.equals(resources[0]);
             mCallback.invoke(mOrigin, true, false);
         }
 
+        @Override
         public void deny() {
             mCallback.invoke(mOrigin, false, false);
         }
@@ -127,6 +150,7 @@
 
     // For simplicity, also treat the read access needed for file:// URLs as a regular
     // PermissionRequest.
+    @SuppressLint("NewApi") // FilePermissionRequest class requires API level 21.
     private class FilePermissionRequest extends PermissionRequest {
         private String mOrigin;
 
@@ -134,14 +158,17 @@
             mOrigin = origin;
         }
 
+        @Override
         public Uri getOrigin() {
             return Uri.parse(mOrigin);
         }
 
+        @Override
         public String[] getResources() {
             return new String[] { WebViewBrowserActivity.RESOURCE_FILE_URL };
         }
 
+        @Override
         public void grant(String[] resources) {
             assert resources.length == 1;
             assert WebViewBrowserActivity.RESOURCE_FILE_URL.equals(resources[0]);
@@ -149,20 +176,62 @@
             WebViewBrowserActivity.this.mWebView.loadUrl(mOrigin);
         }
 
+        @Override
         public void deny() {
             // womp womp
         }
     }
 
+    private static class TracingLogger extends FileOutputStream {
+        private long mByteCount;
+        private long mChunkCount;
+        private final Activity mActivity;
+
+        public TracingLogger(String fileName, Activity activity) throws FileNotFoundException {
+            super(fileName);
+            mActivity = activity;
+        }
+
+        @Override
+        public void write(byte[] chunk) throws IOException {
+            mByteCount += chunk.length;
+            mChunkCount++;
+            super.write(chunk);
+        }
+
+        @Override
+        public void close() throws IOException {
+            super.close();
+            showDialog(mByteCount);
+        }
+
+        private void showDialog(long nbBytes) {
+            StringBuilder info = new StringBuilder();
+            info.append("Tracing data written to file\n");
+            info.append("number of bytes: " + nbBytes);
+
+            mActivity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    AlertDialog dialog = new AlertDialog.Builder(mActivity)
+                                                 .setTitle("Tracing API")
+                                                 .setMessage(info)
+                                                 .setNeutralButton(" OK ", null)
+                                                 .create();
+                    dialog.show();
+                }
+            });
+        }
+    }
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
-            WebView.setWebContentsDebuggingEnabled(true);
-        }
+        WebView.setWebContentsDebuggingEnabled(true);
         setContentView(R.layout.activity_webview_browser);
         mUrlBar = (EditText) findViewById(R.id.url_field);
         mUrlBar.setOnKeyListener(new OnKeyListener() {
+            @Override
             public boolean onKey(View view, int keyCode, KeyEvent event) {
                 if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
                     loadUrlFromUrlBar(view);
@@ -172,13 +241,75 @@
             }
         });
 
+        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
+                .detectAll()
+                .penaltyLog()
+                .penaltyDeath()
+                .build());
+        // Conspicuously omitted: detectCleartextNetwork() and detectFileUriExposure() to permit
+        // http:// and file:// origins.
+        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
+                .detectActivityLeaks()
+                .detectLeakedClosableObjects()
+                .detectLeakedRegistrationObjects()
+                .detectLeakedSqlLiteObjects()
+                .penaltyLog()
+                .penaltyDeath()
+                .build());
+
         createAndInitializeWebView();
 
         String url = getUrlFromIntent(getIntent());
-        if (url != null) {
-            setUrlBarText(url);
-            setUrlFail(false);
-            loadUrlFromUrlBar(mUrlBar);
+        if (url == null) {
+            mWebView.restoreState(savedInstanceState);
+            url = mWebView.getUrl();
+            if (url != null) {
+                // If we have restored state, and that state includes
+                // a loaded URL, we reload. This allows us to keep the
+                // scroll offset, and also doesn't add an additional
+                // navigation history entry.
+                setUrlBarText(url);
+                // The immediately previous loadUrlFromurlbar must
+                // have got as far as calling loadUrl, so there is no
+                // URI parsing error at this point.
+                setUrlFail(false);
+                hideKeyboard(mUrlBar);
+                mWebView.reload();
+                mWebView.requestFocus();
+                return;
+            }
+            // Make sure to load a blank page to make it immediately inspectable with
+            // chrome://inspect.
+            url = "about:blank";
+        }
+        setUrlBarText(url);
+        setUrlFail(false);
+        loadUrlFromUrlBar(mUrlBar);
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle savedInstanceState) {
+        // Deliberately don't catch TransactionTooLargeException here.
+        mWebView.saveState(savedInstanceState);
+
+        // TODO(timav): Remove this hack after http://crbug.com/626202 is fixed.
+        // Drop the saved state of it is too long since Android N and above
+        // can't handle large states without a crash.
+        byte[] webViewState = savedInstanceState.getByteArray(SAVE_RESTORE_STATE_KEY);
+        if (webViewState != null && webViewState.length > MAX_STATE_LENGTH) {
+            savedInstanceState.remove(SAVE_RESTORE_STATE_KEY);
+            String message = String.format(
+                    Locale.US, "Can't save state: %dkb is too long", webViewState.length / 1024);
+            Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
+        }
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (mWebView.canGoBack()) {
+            mWebView.goBack();
+        } else {
+            super.onBackPressed();
         }
     }
 
@@ -210,18 +341,18 @@
                 setUrlBarText(url);
             }
 
+            @SuppressWarnings("deprecation") // because we support api level 19 and up.
             @Override
-            public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
-                String url = request.getUrl().toString();
+            public boolean shouldOverrideUrlLoading(WebView webView, String url) {
                 // "about:" and "chrome:" schemes are internal to Chromium;
                 // don't want these to be dispatched to other apps.
                 if (url.startsWith("about:") || url.startsWith("chrome:")) {
                     return false;
                 }
-                boolean allowLaunchingApps = request.hasGesture() || request.isRedirect();
-                return startBrowsingIntent(WebViewBrowserActivity.this, url, allowLaunchingApps);
+                return startBrowsingIntent(WebViewBrowserActivity.this, url);
             }
 
+            @SuppressWarnings("deprecation") // because we support api level 19 and up.
             @Override
             public void onReceivedError(WebView view, int errorCode, String description,
                     String failingUrl) {
@@ -239,6 +370,13 @@
             @Override
             public void onGeolocationPermissionsShowPrompt(String origin,
                     GeolocationPermissions.Callback callback) {
+                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+                    // Pre Lollipop versions (< api level 21) do not have PermissionRequest,
+                    // hence grant here immediately.
+                    callback.invoke(origin, true, false);
+                    return;
+                }
+
                 onPermissionRequest(new GeoPermissionRequest(origin, callback));
             }
 
@@ -246,6 +384,28 @@
             public void onPermissionRequest(PermissionRequest request) {
                 WebViewBrowserActivity.this.requestPermissionsForPage(request);
             }
+
+            @Override
+            public void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) {
+                if (mFullscreenView != null) {
+                    ((ViewGroup) mFullscreenView.getParent()).removeView(mFullscreenView);
+                }
+                mFullscreenView = view;
+                getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+                getWindow().addContentView(mFullscreenView,
+                        new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                                ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER));
+            }
+
+            @Override
+            public void onHideCustomView() {
+                if (mFullscreenView == null) {
+                    return;
+                }
+                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+                ((ViewGroup) mFullscreenView.getParent()).removeView(mFullscreenView);
+                mFullscreenView = null;
+            }
         });
 
         mWebView = webview;
@@ -256,14 +416,16 @@
 
     // WebKit permissions which can be granted because either they have no associated Android
     // permission or the associated Android permission has been granted
+    @TargetApi(Build.VERSION_CODES.M)
     private boolean canGrant(String webkitPermission) {
         String androidPermission = sPermissions.get(webkitPermission);
-        if (androidPermission == NO_ANDROID_PERMISSION) {
+        if (androidPermission.equals(NO_ANDROID_PERMISSION)) {
             return true;
         }
         return PackageManager.PERMISSION_GRANTED == checkSelfPermission(androidPermission);
     }
 
+    @SuppressLint("NewApi") // PermissionRequest#deny requires API level 21.
     private void requestPermissionsForPage(PermissionRequest request) {
         // Deny any unrecognized permissions.
         for (String webkitPermission : request.getResources()) {
@@ -309,12 +471,14 @@
     }
 
     @Override
+    @SuppressLint("NewApi") // PermissionRequest#deny requires API level 21.
     public void onRequestPermissionsResult(int requestCode,
             String permissions[], int[] grantResults) {
         // Verify that we can now grant all the requested permissions. Note that although grant()
         // takes a list of permissions, grant() is actually all-or-nothing. If there are any
         // requested permissions not included in the granted permissions, all will be denied.
         PermissionRequest request = mPendingRequests.get(requestCode);
+        mPendingRequests.delete(requestCode);
         for (String webkitPermission : request.getResources()) {
             if (!canGrant(webkitPermission)) {
                 request.deny();
@@ -322,21 +486,14 @@
             }
         }
         request.grant(request.getResources());
-        mPendingRequests.delete(requestCode);
     }
 
     public void loadUrlFromUrlBar(View view) {
         String url = mUrlBar.getText().toString();
-        try {
-            URI uri = new URI(url);
-            url = (uri.getScheme() == null) ? "http://" + uri.toString() : uri.toString();
-        } catch (URISyntaxException e) {
-            String message = "<html><body>URISyntaxException: " + e.getMessage() + "</body></html>";
-            mWebView.loadData(message, "text/html", "UTF-8");
-            setUrlFail(true);
-            return;
-        }
-
+        // Parse with android.net.Uri instead of java.net.URI because Uri does no validation. Rather
+        // than failing in the browser, let WebView handle weird URLs. WebView will escape illegal
+        // characters and display error pages for bad URLs like "blah://example.com".
+        if (Uri.parse(url).getScheme() == null) url = "http://" + url;
         setUrlBarText(url);
         setUrlFail(false);
         loadUrl(url);
@@ -347,10 +504,12 @@
         PopupMenu popup = new PopupMenu(this, v);
         popup.setOnMenuItemClickListener(this);
         popup.inflate(R.menu.main_menu);
+        popup.getMenu().findItem(R.id.menu_enable_tracing).setChecked(mEnableTracing);
         popup.show();
     }
 
     @Override
+    @SuppressLint("NewApi") // TracingController related methods require API level 28.
     public boolean onMenuItemClick(MenuItem item) {
         switch(item.getItemId()) {
             case R.id.menu_reset_webview:
@@ -362,6 +521,42 @@
                 }
                 createAndInitializeWebView();
                 return true;
+            case R.id.menu_clear_cache:
+                if (mWebView != null) {
+                    mWebView.clearCache(true);
+                }
+                return true;
+            case R.id.menu_enable_tracing:
+                mEnableTracing = !mEnableTracing;
+                item.setChecked(mEnableTracing);
+                TracingController tracingController = TracingController.getInstance();
+                if (mEnableTracing) {
+                    tracingController.start(
+                            new TracingConfig.Builder()
+                                    .addCategories(TracingConfig.CATEGORIES_WEB_DEVELOPER)
+                                    .setTracingMode(TracingConfig.RECORD_CONTINUOUSLY)
+                                    .build());
+                } else {
+                    StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+                    String outFileName = getFilesDir() + "/webview_tracing.json";
+                    try {
+                        tracingController.stop(new TracingLogger(outFileName, this),
+                                Executors.newSingleThreadExecutor());
+                    } catch (FileNotFoundException e) {
+                        throw new RuntimeException(e);
+                    }
+                    StrictMode.setThreadPolicy(oldPolicy);
+                }
+                return true;
+            case R.id.start_animation_activity:
+                startActivity(new Intent(this, WebViewAnimationTestActivity.class));
+                return true;
+            case R.id.menu_print:
+                PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
+                String jobName = "WebViewShell document";
+                PrintDocumentAdapter printAdapter = mWebView.createPrintDocumentAdapter(jobName);
+                printManager.print(jobName, printAdapter, new PrintAttributes.Builder().build());
+                return true;
             case R.id.menu_about:
                 about();
                 hideKeyboard(mUrlBar);
@@ -371,18 +566,33 @@
         }
     }
 
+    // setGeolocationDatabasePath deprecated in api level 24,
+    // but we still use it because we support api level 19 and up.
+    @SuppressWarnings("deprecation")
     private void initializeSettings(WebSettings settings) {
+        File appcache = null;
+        File geolocation = null;
+
+        StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+        appcache = getDir("appcache", 0);
+        geolocation = getDir("geolocation", 0);
+        StrictMode.setThreadPolicy(oldPolicy);
+
         settings.setJavaScriptEnabled(true);
 
         // configure local storage apis and their database paths.
-        settings.setAppCachePath(getDir("appcache", 0).getPath());
-        settings.setGeolocationDatabasePath(getDir("geolocation", 0).getPath());
-        settings.setDatabasePath(getDir("databases", 0).getPath());
+        settings.setAppCachePath(appcache.getPath());
+        settings.setGeolocationDatabasePath(geolocation.getPath());
 
         settings.setAppCacheEnabled(true);
         settings.setGeolocationEnabled(true);
         settings.setDatabaseEnabled(true);
         settings.setDomStorageEnabled(true);
+
+        // Default layout behavior for chrome on android.
+        settings.setUseWideViewPort(true);
+        settings.setLoadWithOverviewMode(true);
+        settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING);
     }
 
     private void about() {
@@ -404,7 +614,7 @@
                 .setPositiveButton("OK", null)
                 .create();
         dialog.show();
-        dialog.getWindow().setLayout(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
+        dialog.getWindow().setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
     }
 
     // Returns true is a method has no arguments and returns either a boolean or a String.
@@ -461,8 +671,7 @@
             + ")"
             + "(.*)");
 
-    private static boolean startBrowsingIntent(Context context, String url,
-            boolean allowLaunchingApps) {
+    private static boolean startBrowsingIntent(Context context, String url) {
         Intent intent;
         // Perform generic parsing of the URI to turn it into an Intent.
         try {
@@ -492,12 +701,16 @@
         // same application can be opened in the same tab.
         intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
         try {
-            if (allowLaunchingApps) {
-                context.startActivity(intent);
-            }
+            context.startActivity(intent);
             return true;
         } catch (ActivityNotFoundException ex) {
             Log.w(TAG, "No application can handle " + url);
+        } catch (SecurityException ex) {
+            // This can happen if the Activity is exported="true", guarded by a permission, and sets
+            // up an intent filter matching this intent. This is a valid configuration for an
+            // Activity, so instead of crashing, we catch the exception and do nothing. See
+            // https://crbug.com/808494 and https://crbug.com/889300.
+            Log.w(TAG, "SecurityException when starting intent for " + url);
         }
         return false;
     }