chromium/android_webview/test/shell/src/org/chromium/android_webview/shell/AwShellActivity.java

// 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);
    }
}