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