chromium/android_webview/tools/sdk_sandbox/webview_client/src/org/chromium/sdk_sandbox/webview_client/MainActivity.java

// Copyright 2023 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.sdk_sandbox.webview_client;

import static android.app.sdksandbox.SdkSandboxManager.EXTRA_DISPLAY_ID;
import static android.app.sdksandbox.SdkSandboxManager.EXTRA_HEIGHT_IN_PIXELS;
import static android.app.sdksandbox.SdkSandboxManager.EXTRA_HOST_TOKEN;
import static android.app.sdksandbox.SdkSandboxManager.EXTRA_SURFACE_PACKAGE;
import static android.app.sdksandbox.SdkSandboxManager.EXTRA_WIDTH_IN_PIXELS;

import android.app.Activity;
import android.app.sdksandbox.LoadSdkException;
import android.app.sdksandbox.RequestSurfacePackageException;
import android.app.sdksandbox.SandboxedSdk;
import android.app.sdksandbox.SdkSandboxManager;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.OutcomeReceiver;
import android.os.RemoteException;
import android.view.KeyEvent;
import android.view.SurfaceControlViewHost.SurfacePackage;
import android.view.SurfaceView;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;

import org.chromium.sdk_sandbox.webview_sdk.IWebViewSdkApi;

/**
 * This activity allows manual testing of WebView inside the sdk sandbox.
 * There are buttons to:
 * -> load an sdk that has methods to interact with the WebView
 * -> load and display the WebView
 * -> load a url in the WebView specified via a URL bar
 */
public class MainActivity extends Activity {
    private static final String SDK_NAME = "org.chromium.sdk_sandbox.webview_sdk";
    private static final String TAG = "WebViewSdkSandboxActivity";

    private static final Handler sHandler = new Handler(Looper.getMainLooper());

    private SdkSandboxManager mSdkSandboxManager;
    private SandboxedSdk mSandboxedSdk;
    private IWebViewSdkApi mWebViewProxy;
    private boolean mSdkLoaded;
    private boolean mSurfacePackageLoaded;

    private Button mLoadSdkButton;
    private Button mLoadSurfacePackageButton;
    private ImageButton mLoadUrlButton;
    private EditText mUrlBar;
    private SurfaceView mRenderedView;

    private OutcomeReceiver<SandboxedSdk, LoadSdkException> mLoadSdkReceiver =
            new OutcomeReceiver<>() {
                @Override
                public void onResult(SandboxedSdk sandboxedSdk) {
                    mSdkLoaded = true;
                    mSandboxedSdk = sandboxedSdk;
                    mWebViewProxy = IWebViewSdkApi.Stub.asInterface(mSandboxedSdk.getInterface());
                    makeToast("SDK Loaded successfully!");
                    mLoadSdkButton.setText(R.string.unload_sdk_provider);
                }

                @Override
                public void onError(LoadSdkException error) {
                    makeToast(
                            String.format(
                                    "Failed: %s. Error code %s!",
                                    error, error.getLoadSdkErrorCode()));
                    resetStateForLoadSdkButton();
                }
            };

    private OutcomeReceiver<Bundle, RequestSurfacePackageException> mLoadSurfacePackageReceiver =
            new OutcomeReceiver<Bundle, RequestSurfacePackageException>() {
                @Override
                public void onResult(Bundle result) {
                    sHandler.post(
                            () -> {
                                SurfacePackage surfacePackage =
                                        result.getParcelable(
                                                EXTRA_SURFACE_PACKAGE, SurfacePackage.class);
                                mRenderedView.setChildSurfacePackage(surfacePackage);
                                mRenderedView.setVisibility(View.VISIBLE);
                                mSurfacePackageLoaded = true;
                            });
                    makeToast("Rendered surface view");
                }

                @Override
                public void onError(@NonNull RequestSurfacePackageException error) {
                    makeToast(
                            String.format(
                                    "Failed: %s. Error code %s!",
                                    error, error.getRequestSurfacePackageErrorCode()));
                }
            };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mSdkSandboxManager = getApplicationContext().getSystemService(SdkSandboxManager.class);

        mLoadSdkButton = findViewById(R.id.btn_load_sdk);
        mLoadSurfacePackageButton = findViewById(R.id.btn_load_surface_package);

        mLoadUrlButton = findViewById(R.id.btn_load_url);
        mUrlBar = findViewById(R.id.url_field);

        mRenderedView = findViewById(R.id.rendered_view);
        mRenderedView.setZOrderOnTop(true);
        mRenderedView.setVisibility(View.INVISIBLE);

        registerLoadSdkButton();
        registerLoadSurfacePackageButton();
        registerLoadUrlComponents();
    }

    private void registerLoadSdkButton() {
        mLoadSdkButton.setOnClickListener(
                v -> {
                    if (mSdkLoaded) {
                        resetStateForLoadSdkButton();
                        return;
                    }

                    // Register for sandbox death event.
                    mSdkSandboxManager.addSdkSandboxProcessDeathCallback(
                            Runnable::run, () -> makeToast("Sdk Sandbox process died"));

                    Bundle params = new Bundle();
                    runOnUiThread(
                            () ->
                                    mSdkSandboxManager.loadSdk(
                                            SDK_NAME, params, Runnable::run, mLoadSdkReceiver));
                });
    }

    private void resetStateForLoadSdkButton() {
        try {
            mWebViewProxy.destroy();
        } catch (RemoteException e) {
        }
        mSdkSandboxManager.unloadSdk(SDK_NAME);
        mLoadSdkButton.setText(R.string.load_sdk_provider);
        mRenderedView.setVisibility(View.INVISIBLE);
        mSdkLoaded = false;
        mSurfacePackageLoaded = false;
    }

    private void registerLoadSurfacePackageButton() {
        mLoadSurfacePackageButton.setOnClickListener(
                v -> {
                    if (mSdkLoaded) {
                        sHandler.post(
                                () -> {
                                    mSdkSandboxManager.requestSurfacePackage(
                                            SDK_NAME,
                                            getRequestSurfacePackageParams(),
                                            Runnable::run,
                                            mLoadSurfacePackageReceiver);
                                });
                    } else {
                        makeToast("Sdk is not loaded");
                    }
                });
    }

    private void registerLoadUrlComponents() {
        mUrlBar.setOnKeyListener(
                (View view, int keyCode, KeyEvent event) -> {
                    if (keyCode == KeyEvent.KEYCODE_ENTER
                            && event.getAction() == KeyEvent.ACTION_UP) {
                        loadUrl();
                        return true;
                    }
                    return false;
                });

        mLoadUrlButton.setOnClickListener(
                v -> {
                    loadUrl();
                });
    }

    private void loadUrl() {
        if (mSdkLoaded) {
            if (mSurfacePackageLoaded) {
                try {
                    String url = mUrlBar.getText().toString();
                    // 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;
                    mUrlBar.setText(url, TextView.BufferType.EDITABLE);
                    mWebViewProxy.loadUrl(url);
                    // hide keyboard
                    InputMethodManager imm =
                            (InputMethodManager)
                                    mUrlBar.getContext()
                                            .getSystemService(Context.INPUT_METHOD_SERVICE);
                    imm.hideSoftInputFromWindow(mUrlBar.getWindowToken(), 0);
                } catch (RemoteException e) {
                }
            } else {
                makeToast("Surface Package (WebView) is not loaded");
            }
        } else {
            makeToast("Sdk is not loaded");
        }
    }

    private Bundle getRequestSurfacePackageParams() {
        Bundle params = new Bundle();
        params.putInt(EXTRA_WIDTH_IN_PIXELS, mRenderedView.getWidth());
        params.putInt(EXTRA_HEIGHT_IN_PIXELS, mRenderedView.getHeight());
        params.putInt(EXTRA_DISPLAY_ID, getDisplay().getDisplayId());
        params.putBinder(EXTRA_HOST_TOKEN, mRenderedView.getHostToken());
        return params;
    }

    private void makeToast(String message) {
        runOnUiThread(() -> Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show());
    }
}