参考文档:
geckoview版本
引入文档(有坑 下面会给出正确引入方式)
官方示例代码1
官方示例代码2
参考了两位大神的博客和demo:
GeckoView js交互实现
geckoview-jsdemo
引入方式:
maven {
url "https://maven.mozilla.org/maven2/"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility 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.GeckoView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ProgressBar
android: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;
@Override
protected 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() {
@Override
public void onPageStart(GeckoSession session, String url) {
// 网页开始加载时的操作
if (web_progress != null) {
web_progress.setVisibility(View.VISIBLE);
}
}
@Override
public void onPageStop(GeckoSession session, boolean success) {
// 网页加载完成时的操作
if (web_progress != null) {
web_progress.setVisibility(View.GONE);
}
}
@Override
public void onProgressChange(GeckoSession session, int progress) {
// 网页加载进度变化时的操作
if (web_progress != null) {
web_progress.setProgress(progress);
}
}
});
// 权限
session.setPermissionDelegate(new GeckoSession.PermissionDelegate() {
@Override
public 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
@Override
public GeckoResult<Integer> onContentPermissionRequest(@NonNull GeckoSession session, @NonNull ContentPermission perm) {
return GeckoResult.fromValue(ContentPermission.VALUE_ALLOW);
}
@Override
public 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() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
// gather selected media devices and grant access
final 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() {
@Override
public 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
@Override
public 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() {
@Override
public 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) {
// 与前端定义的方法名 示例:callSetToken
case "callSetToken": {
break;
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public 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;
}
@Override
public View getView(final int position, View view, final ViewGroup parent) {
return convertView(position, super.getView(position, view, parent));
}
@Override
public 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;
}
@Override
public 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();
}
}
}
@Override
protected 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" />
<gradient
android: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" />
<gradient
android: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>