阅读量:0
参考文档:
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(); // 自动同意权限 /* final MediaSource videoMediaSource = (videoSpinner != null) ? (MediaSource) videoSpinner.getSelectedItem() : null; final MediaSource audioMediaSource = (audioSpinner != null) ? (MediaSource) audioSpinner.getSelectedItem() : null; callback.grant(videoMediaSource, audioMediaSource); */ } }); 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>