// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.android_webview.shell;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
import org.chromium.android_webview.AwBrowserContext;
import org.chromium.android_webview.AwBrowserProcess;
import org.chromium.android_webview.AwContents;
import org.chromium.android_webview.AwContentsClient;
import org.chromium.android_webview.AwDevToolsServer;
import org.chromium.android_webview.AwGeolocationPermissions;
import org.chromium.android_webview.AwSettings;
import org.chromium.android_webview.JsResultReceiver;
import org.chromium.android_webview.test.AwTestContainerView;
import org.chromium.android_webview.test.NullContentsClient;
import org.chromium.base.CommandLine;
import org.chromium.base.Log;
import org.chromium.base.TraceEvent;
import org.chromium.content_public.browser.NavigationController;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsObserver;
import org.chromium.content_public.common.ContentUrlConstants;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Arrays;
/** This is a lightweight activity for tests that only require WebView functionality. */
public class AwShellActivity extends Activity {
private static final String TAG = "AwShellActivity";
private static final String INITIAL_URL = ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL;
private AwBrowserContext mBrowserContext;
private AwDevToolsServer mDevToolsServer;
private AwTestContainerView mAwTestContainerView;
private WebContents mWebContents;
private NavigationController mNavigationController;
private EditText mUrlTextView;
private ImageButton mPrevButton;
private ImageButton mNextButton;
private final OnBackInvokedCallback mOnBackInvokedCallback =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
? () -> mNavigationController.goBack()
: null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AwShellResourceProvider.registerResources(this);
AwBrowserProcess.loadLibrary(null);
// This flag is deprecated. Print a hint instead.
if (CommandLine.getInstance().hasSwitch(AwShellSwitches.ENABLE_ATRACE)) {
Log.e(TAG, "To trace the test shell, run \"atrace webview\"");
}
TraceEvent.maybeEnableEarlyTracing(/* readCommandLine= */ false);
setContentView(R.layout.testshell_activity);
mAwTestContainerView = createAwTestContainerView();
mWebContents = mAwTestContainerView.getWebContents();
mNavigationController = mWebContents.getNavigationController();
LinearLayout contentContainer = (LinearLayout) findViewById(R.id.content_container);
mAwTestContainerView.setLayoutParams(
new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 1f));
contentContainer.addView(mAwTestContainerView);
mAwTestContainerView.requestFocus();
initializeUrlField();
initializeNavigationButtons();
String startupUrl = getUrlFromIntent(getIntent());
if (TextUtils.isEmpty(startupUrl)) {
startupUrl = INITIAL_URL;
}
mAwTestContainerView.getAwContents().loadUrl(startupUrl);
AwContents.setShouldDownloadFavicons();
mUrlTextView.setText(startupUrl);
mWebContents.addObserver(
new WebContentsObserver() {
@Override
public void navigationEntriesChanged() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (mNavigationController.canGoBack()) {
AwShellActivity.this
.getOnBackInvokedDispatcher()
.registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
mOnBackInvokedCallback);
} else if (!mNavigationController.canGoBack()) {
AwShellActivity.this
.getOnBackInvokedDispatcher()
.unregisterOnBackInvokedCallback(mOnBackInvokedCallback);
}
}
}
});
}
@Override
public void onDestroy() {
if (mDevToolsServer != null) {
mDevToolsServer.setRemoteDebuggingEnabled(false);
mDevToolsServer = null;
}
super.onDestroy();
}
private AwTestContainerView createAwTestContainerView() {
final String supportedModels[] = {
"Pixel 6", "Pixel 6 Pro",
};
boolean useVulkan = Arrays.asList(supportedModels).contains(Build.MODEL);
AwTestContainerView.installDrawFnFunctionTable(useVulkan);
AwBrowserProcess.start();
AwTestContainerView testContainerView = new AwTestContainerView(this, true);
AwContentsClient awContentsClient =
new NullContentsClient() {
private View mCustomView;
@Override
public void handleJsConfirm(
String url, String message, JsResultReceiver receiver) {
String title = "From ";
try {
URL javaUrl = new URL(url);
title += javaUrl.getProtocol() + "://" + javaUrl.getHost();
if (javaUrl.getPort() != -1) {
title += ":" + javaUrl.getPort();
}
} catch (MalformedURLException e) {
title += url;
}
new AlertDialog.Builder(testContainerView.getContext())
.setTitle(title)
.setMessage(message)
.setPositiveButton(
"OK",
new DialogInterface.OnClickListener() {
@Override
public void onClick(
DialogInterface dialogInterface, int i) {
receiver.confirm();
}
})
.setNegativeButton(
"Cancel",
new DialogInterface.OnClickListener() {
@Override
public void onClick(
DialogInterface dialogInterface, int i) {
receiver.cancel();
}
})
.create()
.show();
}
@Override
public void onPageStarted(String url) {
if (mUrlTextView != null) {
mUrlTextView.setText(url);
}
}
@Override
public void onShowCustomView(
View view, AwContentsClient.CustomViewCallback callback) {
getWindow()
.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow()
.addContentView(
view,
new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
Gravity.CENTER));
mCustomView = view;
}
@Override
public void onHideCustomView() {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
FrameLayout decorView = (FrameLayout) getWindow().getDecorView();
decorView.removeView(mCustomView);
mCustomView = null;
}
@Override
public boolean shouldOverrideKeyEvent(KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
return true;
}
return false;
}
@Override
public void onGeolocationPermissionsShowPrompt(
String origin, AwGeolocationPermissions.Callback callback) {
callback.invoke(origin, false, false);
}
};
if (mBrowserContext == null) {
mBrowserContext =
new AwBrowserContext(
AwBrowserContext.getDefault().getNativeBrowserContextPointer());
}
final AwSettings awSettings =
new AwSettings(
/* context= */ this,
/* isAccessFromFileURLsGrantedByDefault= */ false,
/* supportsLegacyQuirks= */ false,
/* allowEmptyDocumentPersistence= */ false,
/* allowGeolocationOnInsecureOrigins= */ true,
/* doNotUpdateSelectionOnMutatingSelectionRange= */ false);
// Required for WebGL conformance tests.
awSettings.setMediaPlaybackRequiresUserGesture(false);
// Allow zoom and fit contents to screen
awSettings.setBuiltInZoomControls(true);
awSettings.setDisplayZoomControls(false);
awSettings.setUseWideViewPort(true);
awSettings.setLoadWithOverviewMode(true);
awSettings.setLayoutAlgorithm(AwSettings.LAYOUT_ALGORITHM_TEXT_AUTOSIZING);
testContainerView.initialize(
new AwContents(
mBrowserContext,
testContainerView,
testContainerView.getContext(),
testContainerView.getInternalAccessDelegate(),
testContainerView.getNativeDrawFunctorFactory(),
awContentsClient,
awSettings));
testContainerView.getAwContents().getSettings().setJavaScriptEnabled(true);
if (mDevToolsServer == null) {
mDevToolsServer = new AwDevToolsServer();
mDevToolsServer.setRemoteDebuggingEnabled(true);
}
return testContainerView;
}
private static String getUrlFromIntent(Intent intent) {
return intent != null ? intent.getDataString() : null;
}
private void setKeyboardVisibilityForUrl(boolean visible) {
InputMethodManager imm =
(InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
if (visible) {
imm.showSoftInput(mUrlTextView, InputMethodManager.SHOW_IMPLICIT);
} else {
imm.hideSoftInputFromWindow(mUrlTextView.getWindowToken(), 0);
}
}
private void initializeUrlField() {
mUrlTextView = (EditText) findViewById(R.id.url);
mUrlTextView.setOnEditorActionListener(
(v, actionId, event) -> {
if ((actionId != EditorInfo.IME_ACTION_GO)
&& (event == null
|| event.getKeyCode() != KeyEvent.KEYCODE_ENTER
|| event.getAction() != KeyEvent.ACTION_DOWN)) {
return false;
}
String url = mUrlTextView.getText().toString();
try {
URI uri = new URI(url);
if (uri.getScheme() == null) {
url = "http://" + uri.toString();
} else {
url = uri.toString();
}
} catch (URISyntaxException e) {
// Ignore syntax errors.
}
mAwTestContainerView.getAwContents().loadUrl(url);
mUrlTextView.clearFocus();
setKeyboardVisibilityForUrl(false);
mAwTestContainerView.requestFocus();
return true;
});
mUrlTextView.setOnFocusChangeListener(
(v, hasFocus) -> {
setKeyboardVisibilityForUrl(hasFocus);
mNextButton.setVisibility(hasFocus ? View.GONE : View.VISIBLE);
mPrevButton.setVisibility(hasFocus ? View.GONE : View.VISIBLE);
if (!hasFocus) {
mUrlTextView.setText(mWebContents.getVisibleUrl().getSpec());
}
});
}
private void initializeNavigationButtons() {
mPrevButton = (ImageButton) findViewById(R.id.prev);
mPrevButton.setOnClickListener(
v -> {
if (mNavigationController.canGoBack()) {
mNavigationController.goBack();
}
});
mNextButton = (ImageButton) findViewById(R.id.next);
mNextButton.setOnClickListener(
v -> {
if (mNavigationController.canGoForward()) {
mNavigationController.goForward();
}
});
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (mNavigationController.canGoBack()) {
mNavigationController.goBack();
return true;
}
}
return super.onKeyUp(keyCode, event);
}
}