1 // Copyright 2019 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.webview_shell; 6 7 import android.app.Activity; 8 import android.content.Context; 9 import android.os.Bundle; 10 import android.os.Message; 11 import android.view.LayoutInflater; 12 import android.view.View; 13 import android.view.ViewGroup; 14 import android.webkit.WebChromeClient; 15 import android.webkit.WebSettings; 16 import android.webkit.WebView; 17 import android.widget.Button; 18 import android.widget.LinearLayout; 19 import android.widget.RelativeLayout; 20 import android.widget.TextView; 21 22 import androidx.annotation.VisibleForTesting; 23 import androidx.webkit.WebViewClientCompat; 24 25 import com.google.common.collect.BiMap; 26 import com.google.common.collect.HashBiMap; 27 28 import org.chromium.base.Log; 29 30 /** 31 * A main activity to handle WPT requests. 32 * 33 * This is currently implemented to support minimum viable implementation such that 34 * multi-window and JavaScript are enabled by default. 35 * Note: multi-window support in this shell is implemented, but due to a bug 36 * in WebView (https://crbug.com/100272), it does not work properly unless a delay is given. 37 */ 38 public class WebPlatformTestsActivity extends Activity { 39 private static final String TAG = "WPTActivity"; 40 private static final boolean DEBUG = false; 41 42 /** 43 * A callback for testing. 44 */ 45 @VisibleForTesting 46 public interface TestCallback { 47 /** Called after child layout is added. */ onChildLayoutAdded(WebView webView)48 void onChildLayoutAdded(WebView webView); 49 /** Called after child layout is removed. */ onChildLayoutRemoved()50 void onChildLayoutRemoved(); 51 } 52 53 private BiMap<ViewGroup, WebView> mLayoutToWebViewBiMap = HashBiMap.create(); 54 55 private LayoutInflater mLayoutInflater; 56 private RelativeLayout mRootLayout; 57 private WebView mWebView; 58 private TestCallback mTestCallback; 59 60 private class MultiWindowWebChromeClient extends WebChromeClient { 61 @Override onCreateWindow( WebView parentWebView, boolean isDialog, boolean isUserGesture, Message resultMsg)62 public boolean onCreateWindow( 63 WebView parentWebView, boolean isDialog, boolean isUserGesture, Message resultMsg) { 64 if (DEBUG) Log.i(TAG, "onCreateWindow"); 65 WebView childWebView = createChildLayoutAndGetNewWebView(parentWebView); 66 WebSettings settings = childWebView.getSettings(); 67 setUpWebSettings(settings); 68 childWebView.setWebViewClient(new WebViewClientCompat() { 69 @Override 70 public void onPageFinished(WebView childWebView, String url) { 71 if (DEBUG) Log.i(TAG, "onPageFinished"); 72 // Once the view has loaded, display its title for debugging. 73 ViewGroup childLayout = mLayoutToWebViewBiMap.inverse().get(childWebView); 74 TextView childTitleText = childLayout.findViewById(R.id.childTitleText); 75 childTitleText.setText(childWebView.getTitle()); 76 } 77 }); 78 childWebView.setWebChromeClient(new MultiWindowWebChromeClient()); 79 // Tell the transport about the new view 80 WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; 81 transport.setWebView(childWebView); 82 resultMsg.sendToTarget(); 83 if (mTestCallback != null) mTestCallback.onChildLayoutAdded(childWebView); 84 return true; 85 } 86 87 @Override onCloseWindow(WebView webView)88 public void onCloseWindow(WebView webView) { 89 ViewGroup childLayout = mLayoutToWebViewBiMap.inverse().get(webView); 90 if (childLayout == mRootLayout) { 91 Log.w(TAG, "Ignoring onCloseWindow() on the top-level webview."); 92 } else { 93 closeChild(childLayout); 94 } 95 } 96 } 97 98 /** Remove and destroy a webview if it exists. */ removeAndDestroyWebView(WebView webView)99 private void removeAndDestroyWebView(WebView webView) { 100 if (webView == null) return; 101 ViewGroup parent = (ViewGroup) webView.getParent(); 102 if (parent != null) parent.removeView(webView); 103 webView.destroy(); 104 } 105 getUrlFromIntent()106 private String getUrlFromIntent() { 107 if (getIntent() == null) return null; 108 return getIntent().getDataString(); 109 } 110 111 @Override onCreate(Bundle savedInstanceState)112 public void onCreate(Bundle savedInstanceState) { 113 super.onCreate(savedInstanceState); 114 WebView.setWebContentsDebuggingEnabled(true); 115 mLayoutInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); 116 setContentView(R.layout.activity_web_platform_tests); 117 mRootLayout = findViewById(R.id.rootLayout); 118 mWebView = mRootLayout.findViewById(R.id.rootWebView); 119 mLayoutToWebViewBiMap.put(mRootLayout, mWebView); 120 121 String url = getUrlFromIntent(); 122 if (url == null) { 123 // This is equivalent to Chrome's WPT setup. 124 setUpMainWebView("about:blank"); 125 } else { 126 Log.w(TAG, 127 "Handling a non-empty intent. This should only be used for testing. URL: " 128 + url); 129 setUpMainWebView(url); 130 } 131 } 132 133 @Override onDestroy()134 protected void onDestroy() { 135 super.onDestroy(); 136 removeAndDestroyWebView(mWebView); 137 mWebView = null; 138 } 139 createChildLayoutAndGetNewWebView(WebView parentWebView)140 private WebView createChildLayoutAndGetNewWebView(WebView parentWebView) { 141 // Add all the child layouts to the root layout such that we can remove 142 // a child layout without affecting any grand child layout. 143 final ViewGroup parentLayout = mRootLayout; 144 // Provide parent such that MATCH_PARENT layout params can work. Ignore the return value 145 // which is parentLayout. 146 mLayoutInflater.inflate(R.layout.activity_web_platform_tests_child, parentLayout); 147 // Choose what has just been added. 148 LinearLayout childLayout = 149 (LinearLayout) parentLayout.getChildAt(parentLayout.getChildCount() - 1); 150 Button childCloseButton = childLayout.findViewById(R.id.childCloseButton); 151 childCloseButton.setOnClickListener((View v) -> { closeChild(childLayout); }); 152 WebView childWebView = childLayout.findViewById(R.id.childWebView); 153 mLayoutToWebViewBiMap.put(childLayout, childWebView); 154 return childWebView; 155 } 156 setUpWebSettings(WebSettings settings)157 private void setUpWebSettings(WebSettings settings) { 158 // Required by WPT. 159 settings.setJavaScriptEnabled(true); 160 // Enable multi-window. 161 settings.setJavaScriptCanOpenWindowsAutomatically(true); 162 settings.setSupportMultipleWindows(true); 163 // Respect "viewport" HTML meta tag. This is false by default, but set to false to be clear. 164 settings.setUseWideViewPort(false); 165 settings.setDomStorageEnabled(true); 166 } 167 setUpMainWebView(String url)168 private void setUpMainWebView(String url) { 169 setUpWebSettings(mWebView.getSettings()); 170 mWebView.setWebChromeClient(new MultiWindowWebChromeClient()); 171 mWebView.loadUrl(url); 172 } 173 closeChild(ViewGroup childLayout)174 private void closeChild(ViewGroup childLayout) { 175 if (DEBUG) Log.i(TAG, "closeChild"); 176 ViewGroup parent = (ViewGroup) childLayout.getParent(); 177 if (parent != null) parent.removeView(childLayout); 178 WebView childWebView = mLayoutToWebViewBiMap.get(childLayout); 179 removeAndDestroyWebView(childWebView); 180 mLayoutToWebViewBiMap.remove(childLayout); 181 if (mTestCallback != null) mTestCallback.onChildLayoutRemoved(); 182 } 183 184 @VisibleForTesting setTestCallback(TestCallback testDelegate)185 public void setTestCallback(TestCallback testDelegate) { 186 mTestCallback = testDelegate; 187 } 188 189 @VisibleForTesting getTestRunnerWebView()190 public WebView getTestRunnerWebView() { 191 return mWebView; 192 } 193 } 194