与网页内容交互

与网页内容和 WebExtensions 交互

GeckoView 允许嵌入式应用程序在 GeckoView 实例中注册和运行 WebExtensions。扩展程序是与网页内容交互的首选方式。

在 GeckoView 中运行扩展程序

与应用程序捆绑的扩展程序可以放在 APK 的 /assets 部分中的一个文件夹中。与普通扩展程序一样,每个与 GeckoView 捆绑的扩展程序都需要一个 manifest.json 文件。

为了定位与 APK 捆绑的文件,GeckoView 提供了一个简写 resource://android/,它指向 APK 的根目录。

例如,resource://android/assets/messaging/ 将指向 APK 中存在的 /assets/messaging/ 文件夹。

注意:每个已安装的扩展程序都需要在 manifest.json 文件中指定一个 idversion

要在 GeckoView 中安装捆绑的扩展程序,只需调用 WebExtensionController.installBuiltIn

runtime.getWebExtensionController()
  .installBuiltIn("resource://android/assets/messaging/")

请注意,扩展程序的生命周期与 GeckoRuntime 实例的生命周期无关。即使应用程序重新启动,扩展程序也会持续存在。每次启动时安装都可以,但速度可能会很慢。为了避免多次安装,您可以使用 WebExtensionRuntime.ensureBuiltIn,它仅在扩展程序尚未安装时才会安装。

runtime.getWebExtensionController()
  .ensureBuiltIn("resource://android/assets/messaging/", "[email protected]")
  .accept(
        extension -> Log.i("MessageDelegate", "Extension installed: " + extension),
        e -> Log.e("MessageDelegate", "Error registering WebExtension", e)
  );

与网页内容通信

GeckoView 允许通过扩展程序与网页进行双向通信。

使用 GeckoView 时,可以使用 本地消息传递 来与浏览器进行通信。

注意:只有当扩展程序的 manifest.json 文件中存在 geckoViewAddons 权限 时,这些 API 才可用。

一次性消息

内容脚本后台脚本 发送消息的最简单方法是使用 runtime.sendNativeMessage

注意:通常,原生扩展程序会使用 原生清单 来定义要使用的原生应用程序标识符。对于 GeckoView,这 *不需要*,setMessageDelegate 中的 nativeApp 参数将用于确定使用什么原生应用程序字符串。

消息传递示例

要接收来自后台脚本的消息,请在 WebExtension 对象上调用 setMessageDelegate

extension.setMessageDelegate(messageDelegate, "browser");

SessionController.setMessageDelegate 允许应用程序接收来自内容脚本的消息。

session.getWebExtensionController()
    .setMessageDelegate(extension, messageDelegate, "browser");

注意:上面的代码中的 "browser" 参数决定了扩展程序可以使用什么原生应用程序 ID 来发送本地消息。

注意:扩展程序只能在应用程序通过在 manifest.json 文件中添加 nativeMessagingFromContent 明确授权的情况下,才能从内容脚本发送消息,例如:

"permissions": [
  "nativeMessaging",
  "nativeMessagingFromContent",
  "geckoViewAddons"
]

示例

让我们设置一个活动,该活动注册位于 APK 的 /assets/messaging/ 文件夹中的扩展程序。此活动将设置一个 MessageDelegate,该委托将用于与网页内容通信。

您可以在此处找到完整的示例:MessagingExample

Activity.java
WebExtension.MessageDelegate messageDelegate = new WebExtension.MessageDelegate() {
    @Nullable
    public GeckoResult<Object> onMessage(final @NonNull String nativeApp,
                                         final @NonNull Object message,
                                         final @NonNull WebExtension.MessageSender sender) {
        // The sender object contains information about the session that
        // originated this message and can be used to validate that the message
        // has been sent from the expected location.

        // Be careful when handling the type of message as it depends on what
        // type of object was sent from the WebExtension script.
        if (message instanceof JSONObject) {
            // Do something with message
        }
        return null;
    }
};

// Let's make sure the extension is installed
runtime.getWebExtensionController()
        .ensureBuiltIn(EXTENSION_LOCATION, "[email protected]").accept(
            // Set delegate that will receive messages coming from this extension.
            extension -> session.getWebExtensionController()
                    .setMessageDelegate(extension, messageDelegate, "browser"),
            // Something bad happened, let's log an error
            e -> Log.e("MessageDelegate", "Error registering extension", e)
        );

现在,将 geckoViewAddonsnativeMessagingnativeMessagingFromContent 权限添加到您的 manifest.json 文件中。

/assets/messaging/manifest.json
{
  "manifest_version": 2,
  "name": "messaging",
  "version": "1.0",
  "description": "Example messaging web extension.",
  "browser_specific_settings": {
    "gecko": {
      "id": "[email protected]"
    }
  },
  "content_scripts": [
    {
      "matches": ["*://*.twitter.com/*"],
      "js": ["messaging.js"]
    }
  ],
  "permissions": [
    "nativeMessaging",
    "nativeMessagingFromContent",
    "geckoViewAddons"
  ]
}

最后,编写一个内容脚本,该脚本将在发生特定事件时向应用程序发送消息。例如,您可以在页面上找到 WPA 清单 时发送消息。请注意,我们用于 sendNativeMessagenativeApp 标识符与 Activity.javasetMessageDelegate 调用的标识符相同。

/assets/messaging/messaging.js
let manifest = document.querySelector("head > link[rel=manifest]");
if (manifest) {
     fetch(manifest.href)
        .then(response => response.json())
        .then(json => {
             let message = {type: "WPAManifest", manifest: json};
             browser.runtime.sendNativeMessage("browser", message);
        });
}

您可以在上面 MessageDelegate 中的 onMessage 方法中处理此消息。

@Nullable
public GeckoResult<Object> onMessage(final @NonNull String nativeApp,
                                     final @NonNull Object message,
                                     final @NonNull WebExtension.MessageSender sender) {
    if (message instanceof JSONObject) {
        JSONObject json = (JSONObject) message;
        try {
            if (json.has("type") && "WPAManifest".equals(json.getString("type"))) {
                JSONObject manifest = json.getJSONObject("manifest");
                Log.d("MessageDelegate", "Found WPA manifest: " + manifest);
            }
        } catch (JSONException ex) {
            Log.e("MessageDelegate", "Invalid manifest", ex);
        }
    }
    return null;
}

请注意,在内容脚本的情况下,sender.session 将是消息来源的 GeckoSession 实例的引用。对于后台脚本,sender.session 将始终为 null

还要注意,message 的类型将取决于从扩展程序发送的内容。

当扩展程序发送 JavaScript 对象时,message 的类型将为 JSONObject,但如果扩展程序发送一个基本类型,例如:

runtime.browser.sendNativeMessage("browser", "Hello World!");

message 的类型将为 java.util.String

基于连接的消息传递

对于更复杂的情况或当您想 *从* 应用程序向扩展程序发送消息时,runtime.connectNative 是合适的 API。

connectNative 返回一个 runtime.Port,可用于向应用程序发送消息。在应用程序端,实现 MessageDelegate#onConnect 将允许您接收一个 Port 对象,该对象可用于接收和发送消息到扩展程序。

以下示例可以在 此处 找到。

对于此示例,扩展程序端将执行以下操作

  • 使用 connectNative 在后台脚本上打开一个端口

  • 侦听端口并在控制台中记录接收到的每条消息

  • 在打开端口后立即发送一条消息。

/assets/messaging/background.js

// Establish connection with app
let port = browser.runtime.connectNative("browser");
port.onMessage.addListener(response => {
    // Let's just echo the message back
    port.postMessage(`Received: ${JSON.stringify(response)}`);
});
port.postMessage("Hello from WebExtension!");

在应用程序端,遵循上面 示例onConnect 将把 Port 对象存储在一个成员变量中,然后在需要时使用它。

private WebExtension.Port mPort;

@Override
protected void onCreate(Bundle savedInstanceState) {
    // ... initialize GeckoView

    // This delegate will handle all communications from and to a specific Port
    // object
    WebExtension.PortDelegate portDelegate = new WebExtension.PortDelegate() {
        public WebExtension.Port port = null;

        public void onPortMessage(final @NonNull Object message,
                                  final @NonNull WebExtension.Port port) {
            // This method will be called every time a message is sent from the
            // extension through this port. For now, let's just log a
            // message.
            Log.d("PortDelegate", "Received message from WebExtension: "
                    + message);
        }

        public void onDisconnect(final @NonNull WebExtension.Port port) {
            // After this method is called, this port is not usable anymore.
            if (port == mPort) {
                mPort = null;
            }
        }
    };

    // This delegate will handle requests to open a port coming from the
    // extension
    WebExtension.MessageDelegate messageDelegate = new WebExtension.MessageDelegate() {
        @Nullable
        public void onConnect(final @NonNull WebExtension.Port port) {
            // Let's store the Port object in a member variable so it can be
            // used later to exchange messages with the WebExtension.
            mPort = port;

            // Registering the delegate will allow us to receive messages sent
            // through this port.
            mPort.setDelegate(portDelegate);
        }
    };

    runtime.getWebExtensionController()
        .ensureBuiltIn("resource://android/assets/messaging/", "[email protected]")
        .accept(
            // Register message delegate for background script
            extension -> extension.setMessageDelegate(messageDelegate, "browser"),
            e -> Log.e("MessageDelegate", "Error registering WebExtension", e)
        );

    // ... other
}

例如,让我们在用户长按虚拟键盘上的某个键(例如后退按钮)时向扩展程序发送一条消息。

@Override
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
    if (mPort == null) {
        // No extension registered yet, let's ignore this message
        return false;
    }

    JSONObject message = new JSONObject();
    try {
        message.put("keyCode", keyCode);
        message.put("event", KeyEvent.keyCodeToString(event.getKeyCode()));
    } catch (JSONException ex) {
        throw new RuntimeException(ex);
    }

    mPort.postMessage(message);
    return true;
}

这允许应用程序和扩展程序之间的双向通信。