Android 使用 GeckoView 并实现 js 交互、权限交互

server/2024/9/25 4:29:18/

参考文档:

geckoview版本
引入文档(有坑 下面会给出正确引入方式)
官方示例代码1
官方示例代码2

参考了两位大神的博客和demo:

GeckoView js交互实现
geckoview-jsdemo

引入方式:

        maven {url "https://maven.mozilla.org/maven2/"}
    compileOptions {sourceCompatibility JavaVersion.VERSION_1_8targetCompatibility JavaVersion.VERSION_1_8}
implementation 'org.mozilla.geckoview:geckoview-arm64-v8a:111.0.20230309232128'

使用方式:

控件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><org.mozilla.geckoview.GeckoViewandroid:id="@+id/web_view"android:layout_width="match_parent"android:layout_height="match_parent" /><ProgressBarandroid:id="@+id/web_progress"style="@style/Web.ProgressBar.Horizontal"android:layout_width="match_parent"android:layout_height="wrap_content"android:visibility="visible"tools:progress="50" /></RelativeLayout>

初始化及配置


import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;import android.Manifest;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.content.res.TypedArray;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.Spinner;
import android.widget.TextView;import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.geckoview.GeckoResult;
import org.mozilla.geckoview.GeckoRuntime;
import org.mozilla.geckoview.GeckoRuntimeSettings;
import org.mozilla.geckoview.GeckoSession;
import org.mozilla.geckoview.GeckoSessionSettings;
import org.mozilla.geckoview.GeckoView;
import org.mozilla.geckoview.WebExtension;
import java.util.Locale;public class MainActivity extends AppCompatActivity {private static final String TAG = "MainActivityTag";// 权限回调码private static final int CAMERA_PERMISSION_REQUEST_CODE = 1000;// web - 测试环境private static final String WEB_URL = "https://xxx.xxx.com/";private static final String EXTENSION_LOCATION = "resource://android/assets/messaging/";private static final String EXTENSION_ID = "messaging@example.com";private static GeckoRuntime sRuntime = null;private GeckoSession session;private static WebExtension.Port mPort;private GeckoSession.PermissionDelegate.Callback mCallback;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);setupGeckoView();}private void setupGeckoView() {// 初始化控件GeckoView geckoView = findViewById(R.id.gecko_view);ProgressBar web_progress = findViewById(R.id.web_progress);if (sRuntime == null) {GeckoRuntimeSettings.Builder builder = new GeckoRuntimeSettings.Builder().allowInsecureConnections(GeckoRuntimeSettings.ALLOW_ALL).javaScriptEnabled(true).doubleTapZoomingEnabled(true).inputAutoZoomEnabled(true).forceUserScalableEnabled(true).aboutConfigEnabled(true).loginAutofillEnabled(true).webManifest(true).consoleOutput(true).remoteDebuggingEnabled(BuildConfig.DEBUG).debugLogging(BuildConfig.DEBUG);sRuntime = GeckoRuntime.create(this, builder.build());}// 建立交互installExtension();session = new GeckoSession();GeckoSessionSettings settings = session.getSettings();settings.setAllowJavascript(true);settings.setUserAgentMode(GeckoSessionSettings.USER_AGENT_MODE_MOBILE);session.getPanZoomController().setIsLongpressEnabled(false);// 监听网页加载进度session.setProgressDelegate(new GeckoSession.ProgressDelegate() {@Overridepublic void onPageStart(GeckoSession session, String url) {// 网页开始加载时的操作if (web_progress != null) {web_progress.setVisibility(View.VISIBLE);}}@Overridepublic void onPageStop(GeckoSession session, boolean success) {// 网页加载完成时的操作if (web_progress != null) {web_progress.setVisibility(View.GONE);}}@Overridepublic void onProgressChange(GeckoSession session, int progress) {// 网页加载进度变化时的操作if (web_progress != null) {web_progress.setProgress(progress);}}});// 权限session.setPermissionDelegate(new GeckoSession.PermissionDelegate() {@Overridepublic void onAndroidPermissionsRequest(@NonNull final GeckoSession session,final String[] permissions,@NonNull final Callback callback) {mCallback = callback;if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED|| ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {ActivityCompat.requestPermissions(MainActivity.this, permissions, CAMERA_PERMISSION_REQUEST_CODE);} else {callback.grant();}}@Nullable@Overridepublic GeckoResult<Integer> onContentPermissionRequest(@NonNull GeckoSession session, @NonNull ContentPermission perm) {return GeckoResult.fromValue(ContentPermission.VALUE_ALLOW);}@Overridepublic void onMediaPermissionRequest(@NonNull final GeckoSession session,@NonNull final String uri,final MediaSource[] video,final MediaSource[] audio,@NonNull final MediaCallback callback) {final String host = Uri.parse(uri).getAuthority();final String title;if (audio == null) {title = getString(R.string.request_video, host);} else if (video == null) {title = getString(R.string.request_audio, host);} else {title = getString(R.string.request_media, host);}String[] videoNames = normalizeMediaName(video);String[] audioNames = normalizeMediaName(audio);final AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);final LinearLayout container = addStandardLayout(builder, title, null);final Spinner videoSpinner;if (video != null) {videoSpinner = addMediaSpinner(builder.getContext(), container, video, videoNames); // create spinner and add to alert UI} else {videoSpinner = null;}final Spinner audioSpinner;if (audio != null) {audioSpinner = addMediaSpinner(builder.getContext(), container, audio, audioNames); // create spinner and add to alert UI} else {audioSpinner = null;}builder.setNegativeButton(android.R.string.cancel, null).setPositiveButton(android.R.string.ok,new DialogInterface.OnClickListener() {@Overridepublic void onClick(final DialogInterface dialog, final int which) {// gather selected media devices and grant accessfinal MediaSource video = (videoSpinner != null)? (MediaSource) videoSpinner.getSelectedItem() : null;final MediaSource audio = (audioSpinner != null)? (MediaSource) audioSpinner.getSelectedItem() : null;callback.grant(video, audio);}});final AlertDialog dialog = builder.create();dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {@Overridepublic void onDismiss(final DialogInterface dialog) {callback.reject();}});dialog.show();}});session.open(sRuntime);geckoView.setSession(session);// 打开web地址session.loadUri(WEB_URL);}/*** 建立交互*/private void installExtension() {sRuntime.getWebExtensionController().ensureBuiltIn(EXTENSION_LOCATION, EXTENSION_ID).accept(extension -> {Log.i(TAG, "Extension installed: " + extension);runOnUiThread(() -> {assert extension != null;extension.setMessageDelegate(mMessagingDelegate, "Android");});},e -> Log.e(TAG, "Error registering WebExtension", e));}private final WebExtension.MessageDelegate mMessagingDelegate = new WebExtension.MessageDelegate() {@Nullable@Overridepublic void onConnect(@NonNull WebExtension.Port port) {Log.e(TAG, "MessageDelegate onConnect");mPort = port;mPort.setDelegate(mPortDelegate);}};/*** 接收 JS 发送的消息*/private final WebExtension.PortDelegate mPortDelegate = new WebExtension.PortDelegate() {@Overridepublic void onPortMessage(final @NonNull Object message,final @NonNull WebExtension.Port port) {Log.e(TAG, "from extension: " + message);try {
//                ToastUtils.showLong("收到js调用: " + message);if (message instanceof JSONObject) {JSONObject jsonobject = (JSONObject) message;/** jsonobject 格式**  {*    "action": "JSBridge",*    "data": {*          "args":"字符串",*          "function":"方法名"*    }*  }*/String action = jsonobject.getString("action");if ("JSBridge".equals(action)) {JSONObject data = jsonobject.getJSONObject("data");String function = data.getString("function");if (!TextUtils.isEmpty(function)) {String args = data.getString("args");switch (function) {// 与前端定义的方法名 示例:callSetTokencase "callSetToken": {break;}}}}}} catch (Exception e) {e.printStackTrace();}}@Overridepublic void onDisconnect(final @NonNull WebExtension.Port port) {Log.e(TAG, "MessageDelegate:onDisconnect");if (port == mPort) {mPort = null;}}};/*** 向 js 发送数据 示例:evaluateJavascript("callStartUpload", "startUpload");** @param methodName 定义的方法名* @param data       发送的数据*/private void evaluateJavascript(String methodName, String data) {try {long id = System.currentTimeMillis();JSONObject message = new JSONObject();message.put("action", "evalJavascript");message.put("data", "window." + methodName + "('" + data + "')");message.put("id", id);mPort.postMessage(message);Log.e(TAG,"mPort.postMessage:" + message);} catch (JSONException ex) {throw new RuntimeException(ex);}}/*** web 端:** 接收消息示例:window.callStartUpload = function(data){console.log(data)}** 发送消息示例:* if(typeof window.JSBridge !== 'undefined'){*   window.JSBridge.postMessage({function:name, args})* }**/private int getViewPadding(final AlertDialog.Builder builder) {final TypedArray attr =builder.getContext().obtainStyledAttributes(new int[]{android.R.attr.listPreferredItemPaddingLeft});final int padding = attr.getDimensionPixelSize(0, 1);attr.recycle();return padding;}private LinearLayout addStandardLayout(final AlertDialog.Builder builder, final String title, final String msg) {final ScrollView scrollView = new ScrollView(builder.getContext());final LinearLayout container = new LinearLayout(builder.getContext());final int horizontalPadding = getViewPadding(builder);final int verticalPadding = (msg == null || msg.isEmpty()) ? horizontalPadding : 0;container.setOrientation(LinearLayout.VERTICAL);container.setPadding(/* left */ horizontalPadding, /* top */ verticalPadding,/* right */ horizontalPadding, /* bottom */ verticalPadding);scrollView.addView(container);builder.setTitle(title).setMessage(msg).setView(scrollView);return container;}private Spinner addMediaSpinner(final Context context,final ViewGroup container,final GeckoSession.PermissionDelegate.MediaSource[] sources,final String[] sourceNames) {final ArrayAdapter<GeckoSession.PermissionDelegate.MediaSource> adapter =new ArrayAdapter<GeckoSession.PermissionDelegate.MediaSource>(context, android.R.layout.simple_spinner_item) {private View convertView(final int position, final View view) {if (view != null) {final GeckoSession.PermissionDelegate.MediaSource item = getItem(position);((TextView) view).setText(sourceNames != null ? sourceNames[position] : item.name);}return view;}@Overridepublic View getView(final int position, View view, final ViewGroup parent) {return convertView(position, super.getView(position, view, parent));}@Overridepublic View getDropDownView(final int position, final View view, final ViewGroup parent) {return convertView(position, super.getDropDownView(position, view, parent));}};adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);adapter.addAll(sources);final Spinner spinner = new Spinner(context);spinner.setAdapter(adapter);spinner.setSelection(0);container.addView(spinner);return spinner;}private String[] normalizeMediaName(final GeckoSession.PermissionDelegate.MediaSource[] sources) {if (sources == null) {return null;}String[] res = new String[sources.length];for (int i = 0; i < sources.length; i++) {final int mediaSource = sources[i].source;final String name = sources[i].name;if (GeckoSession.PermissionDelegate.MediaSource.SOURCE_CAMERA == mediaSource) {if (name.toLowerCase(Locale.ROOT).contains("front")) {res[i] = getString(R.string.media_front_camera);} else {res[i] = getString(R.string.media_back_camera);}} else if (!name.isEmpty()) {res[i] = name;} else if (GeckoSession.PermissionDelegate.MediaSource.SOURCE_MICROPHONE == mediaSource) {res[i] = getString(R.string.media_microphone);} else {res[i] = getString(R.string.media_other);}}return res;}@Overridepublic void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {super.onRequestPermissionsResult(requestCode, permissions, grantResults);if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {// 授予权限mCallback.grant();} else {// 拒绝权限mCallback.reject();}}}@Overrideprotected void onDestroy() {super.onDestroy();if (session != null) {session.close();}}
}

资源文件配置:

在assets下新建:messaging 文件夹

在这里插入图片描述
.eslintrc.js

/* This Source Code Form is subject to the terms of the Mozilla Public* License, v. 2.0. If a copy of the MPL was not distributed with this* file, You can obtain one at http://mozilla.org/MPL/2.0/. */"use strict";module.exports = {env: {webextensions: true,},
};

background.js

// Establish connection with app
'use strict';
const port = browser.runtime.connectNative("Android");async function sendMessageToTab(message) {try {let tabs = await browser.tabs.query({})console.log(`background:tabs:${tabs}`)return await browser.tabs.sendMessage(tabs[tabs.length - 1].id,message)} catch (e) {console.log(`background:sendMessageToTab:req:error:${e}`)return e.toString();}
}
//监听 app message
port.onMessage.addListener(request => {let action = request.action;if(action === "evalJavascript") {sendMessageToTab(request).then((resp) => {port.postMessage(resp);}).catch((e) => {console.log(`background:sendMessageToTab:resp:error:${e}`)});}
})//接收 content.js message
browser.runtime.onMessage.addListener((data, sender) => {let action = data.action;console.log("background:content:onMessage:" + action);if (action === 'JSBridge') {port.postMessage(data);}return Promise.resolve('done');
})

content.js

console.log(`content:start`);
let JSBridge = {postMessage: function (message) {browser.runtime.sendMessage({action: "JSBridge",data: message});}
}
window.wrappedJSObject.JSBridge = cloneInto(JSBridge,window,{ cloneFunctions: true });browser.runtime.onMessage.addListener((data, sender) => {console.log("content:eval:" + data);if (data.action === 'evalJavascript') {let evalCallBack = {id: data.id,action: "evalJavascript",}try {let result = window.eval(data.data);console.log("content:eval:result" + result);if (result) {evalCallBack.data = result;} else {evalCallBack.data = "";}} catch (e) {evalCallBack.data = e.toString();return Promise.resolve(evalCallBack);}return Promise.resolve(evalCallBack);}
});

manifest.json

{"manifest_version": 2,"name": "messaging","description": "Uses the proxy API to block requests to specific hosts.","version": "3.0","browser_specific_settings": {"gecko": {"strict_min_version": "65.0","id": "messaging@example.com"}},"content_scripts": [{"matches": ["<all_urls>"],"js": ["content.js"],"run_at": "document_start"}],"background": {"scripts": ["background.js"]},"permissions": ["nativeMessaging","nativeMessagingFromContent","geckoViewAddons","webNavigation","geckoview","tabs","<all_urls>"],"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
}

其他资源文件:

themes.xml

    <!-- WebView进度条 --><style name="Web.ProgressBar.Horizontal" parent="@android:style/Widget.ProgressBar.Horizontal"><item name="android:progressDrawable">@drawable/web_view_progress</item><item name="android:minHeight">2dp</item><item name="android:maxHeight">2dp</item></style>

web_view_progress

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"><item android:id="@android:id/background"><shape><corners android:radius="0dp" /><gradientandroid:angle="270"android:centerY="0.75"android:endColor="#A0B3CF"android:startColor="#A0B3CF" /></shape></item><item android:id="@android:id/progress"><clip><shape><corners android:radius="0dp" /><gradientandroid:angle="270"android:endColor="@color/colorPrimary"android:startColor="@color/colorPrimary" /></shape></clip></item></layer-list>

colors.xml

    <color name="colorPrimary">#FF2673FF</color>

strings.xml

    <string name="device_sharing_microphone">麦克风打开</string><string name="device_sharing_camera">摄像头打开</string><string name="device_sharing_camera_and_mic">摄像头和麦克风打开</string><string name="media_back_camera">背面摄像头</string><string name="media_front_camera">前置摄像头</string><string name="media_microphone">麦克风</string><string name="media_other">未知来源</string><string name="request_video">与共享视频 "%1$s"</string><string name="request_audio">与共享音频 "%1$s"</string><string name="request_media">与共享视频和音频 "%1$s"</string>

http://www.ppmy.cn/server/10373.html

相关文章

go语言并发实战——日志收集系统(六) 编写日志收集系统客户端

上节回顾 在上一篇文章中我们介绍了编写客户端的四个步骤&#xff0c;分别是&#xff1a; 读取配置文件&#xff0c;寻找日志路径初始化服务根据日志路径l来收集日志将收集到的日志发送Kafka中 关于上述的内容博主画了一个思维导图(有点丑&#xff0c;大家勉强看看&#xff0…

联邦学习中的差分隐私与同态加密

PrimiHub一款由密码学专家团队打造的开源隐私计算平台&#xff0c;专注于分享数据安全、密码学、联邦学习、同态加密等隐私计算领域的技术和内容。 联邦学习中的差分隐私与同态加密 随着数据的爆炸式增长和对个人隐私保护意识的提高&#xff0c;隐私保护在机器学习领域中变得愈…

quick3-hydra

kali和quick3都设置NAT模式 quick3easy漏洞挖掘、越权测试、python爬虫、脚本编写、hydra 爆破、凭据收集 主机发现 netdiscover -i eth0 -r 192.168.44.193/24服务探测 nmap -sV -A -T4 -p- 192.168.44.133得到只开放了80和22端口 访问80web dirb目录扫描 找到后台http:/…

xcode c++项目设置运行时参数

在 Xcode 项目中&#xff0c;你可以通过配置 scheme 来指定在运行时传递的参数。以下是在 Xcode 中设置运行时参数的步骤&#xff1a; 打开 Xcode&#xff0c;并打开你的项目。在 Xcode 菜单栏中&#xff0c;选择 "Product" -> "Scheme" -> "E…

pnpm v9 正式发布,已停止 Node.js v16 支持

近日&#xff0c;pnpm v9.0.0 正式发布&#xff01;带来了一系列重大更新和改进&#xff0c;让你的开发体验更加顺畅。现在可以通过 npm install -g pnpm 来升级 pnpm 到最新版本&#xff0c;接下来让我们一起看一下 pnpm v9 有哪些亮眼的新功能吧。 &#x1f525; Node.js 支持…

STM32使用HAL库解码433遥控芯片EV1527

1、首先了解一下433遥控芯片ev1527的基本资料&#xff1a; 这是他编码的关键信息&#xff1a; 也就是说&#xff0c;一帧数据是&#xff1a;一个同步码20位内码4位按键码。 内码20位2^201048576个地址。 发送就是一帧数据接一帧数据不间断发送。 2、解码思路 从上面的帧结构…

开发同城O2O跑腿系统源码:构建高效便捷的本地服务平台教程

为了满足用户对便捷的需求&#xff0c;今天我们将一同探讨如何开发一个高效便捷的同城O2O跑腿系统&#xff0c;以构建一个功能全面、操作简单的本地服务平台。 一、确定需求和功能 在开发同城O2O跑腿系统之前&#xff0c;首先需要明确系统的需求和功能。用户可以通过该系统发布…

什么?双核A7双网口核心板只要49?

“性价比之王” 触觉智能IDO-SOM2D0X系列基于SigmaStar SSD201/202 SoC的超小SOM模组&#xff0c;双核A7 1.2GHz主频&#xff0c;1080P视频解码&#xff0c;支持MIPI/RGB显示接口&#xff0c;支持双以太网&#xff0c;支持SDIO/USB/SPI/I2C/UART/DMIC/I2S&#xff0c;集成音频C…