参考文档:
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>
初始化及配置
importandroidx.annotation.NonNull;importandroidx.annotation.Nullable;importandroidx.appcompat.app.AlertDialog;importandroidx.appcompat.app.AppCompatActivity;importandroidx.core.app.ActivityCompat;importandroidx.core.content.ContextCompat;importandroid.Manifest;importandroid.content.Context;importandroid.content.DialogInterface;importandroid.content.pm.PackageManager;importandroid.content.res.TypedArray;importandroid.net.Uri;importandroid.os.Bundle;importandroid.text.TextUtils;importandroid.util.Log;importandroid.view.View;importandroid.view.ViewGroup;importandroid.widget.ArrayAdapter;importandroid.widget.LinearLayout;importandroid.widget.ProgressBar;importandroid.widget.ScrollView;importandroid.widget.Spinner;importandroid.widget.TextView;importorg.json.JSONException;importorg.json.JSONObject;importorg.mozilla.geckoview.GeckoResult;importorg.mozilla.geckoview.GeckoRuntime;importorg.mozilla.geckoview.GeckoRuntimeSettings;importorg.mozilla.geckoview.GeckoSession;importorg.mozilla.geckoview.GeckoSessionSettings;importorg.mozilla.geckoview.GeckoView;importorg.mozilla.geckoview.WebExtension;importjava.util.Locale;publicclassMainActivityextendsAppCompatActivity{privatestaticfinalStringTAG="MainActivityTag";// 权限回调码privatestaticfinalintCAMERA_PERMISSION_REQUEST_CODE=1000;// web - 测试环境privatestaticfinalStringWEB_URL="https://xxx.xxx.com/";privatestaticfinalStringEXTENSION_LOCATION="resource://android/assets/messaging/";privatestaticfinalStringEXTENSION_ID="[email protected]";privatestaticGeckoRuntime sRuntime =null;privateGeckoSession session;privatestaticWebExtension.Port mPort;privateGeckoSession.PermissionDelegate.Callback mCallback;@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);setupGeckoView();}privatevoidsetupGeckoView(){// 初始化控件GeckoView geckoView =findViewById(R.id.gecko_view);ProgressBar web_progress =findViewById(R.id.web_progress);if(sRuntime ==null){GeckoRuntimeSettings.Builder builder =newGeckoRuntimeSettings.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 =newGeckoSession();GeckoSessionSettings settings = session.getSettings();
settings.setAllowJavascript(true);
settings.setUserAgentMode(GeckoSessionSettings.USER_AGENT_MODE_MOBILE);
session.getPanZoomController().setIsLongpressEnabled(false);// 监听网页加载进度
session.setProgressDelegate(newGeckoSession.ProgressDelegate(){@OverridepublicvoidonPageStart(GeckoSession session,String url){// 网页开始加载时的操作if(web_progress !=null){
web_progress.setVisibility(View.VISIBLE);}}@OverridepublicvoidonPageStop(GeckoSession session,boolean success){// 网页加载完成时的操作if(web_progress !=null){
web_progress.setVisibility(View.GONE);}}@OverridepublicvoidonProgressChange(GeckoSession session,int progress){// 网页加载进度变化时的操作if(web_progress !=null){
web_progress.setProgress(progress);}}});// 权限
session.setPermissionDelegate(newGeckoSession.PermissionDelegate(){@OverridepublicvoidonAndroidPermissionsRequest(@NonNullfinalGeckoSession session,finalString[] permissions,@NonNullfinalCallback 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@OverridepublicGeckoResult<Integer>onContentPermissionRequest(@NonNullGeckoSession session,@NonNullContentPermission perm){returnGeckoResult.fromValue(ContentPermission.VALUE_ALLOW);}@OverridepublicvoidonMediaPermissionRequest(@NonNullfinalGeckoSession session,@NonNullfinalString uri,finalMediaSource[] video,finalMediaSource[] audio,@NonNullfinalMediaCallback callback){finalString host =Uri.parse(uri).getAuthority();finalString title;if(audio ==null){
title =getString(R.string.request_video, host);}elseif(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);finalAlertDialog.Builder builder =newAlertDialog.Builder(MainActivity.this);finalLinearLayout container =addStandardLayout(builder, title,null);finalSpinner videoSpinner;if(video !=null){
videoSpinner =addMediaSpinner(builder.getContext(), container, video, videoNames);// create spinner and add to alert UI}else{
videoSpinner =null;}finalSpinner 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,newDialogInterface.OnClickListener(){@OverridepublicvoidonClick(finalDialogInterface dialog,finalint which){// gather selected media devices and grant accessfinalMediaSource video =(videoSpinner !=null)?(MediaSource) videoSpinner.getSelectedItem():null;finalMediaSource audio =(audioSpinner !=null)?(MediaSource) audioSpinner.getSelectedItem():null;
callback.grant(video, audio);}});finalAlertDialog dialog = builder.create();
dialog.setOnDismissListener(newDialogInterface.OnDismissListener(){@OverridepublicvoidonDismiss(finalDialogInterface dialog){
callback.reject();}});
dialog.show();}});
session.open(sRuntime);
geckoView.setSession(session);// 打开web地址
session.loadUri(WEB_URL);}/**
* 建立交互
*/privatevoidinstallExtension(){
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));}privatefinalWebExtension.MessageDelegate mMessagingDelegate =newWebExtension.MessageDelegate(){@Nullable@OverridepublicvoidonConnect(@NonNullWebExtension.Port port){Log.e(TAG,"MessageDelegate onConnect");
mPort = port;
mPort.setDelegate(mPortDelegate);}};/**
* 接收 JS 发送的消息
*/privatefinalWebExtension.PortDelegate mPortDelegate =newWebExtension.PortDelegate(){@OverridepublicvoidonPortMessage(final@NonNullObject message,[email protected] port){Log.e(TAG,"from extension: "+ message);try{// ToastUtils.showLong("收到js调用: " + message);if(message instanceofJSONObject){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();}}@OverridepublicvoidonDisconnect([email protected] port){Log.e(TAG,"MessageDelegate:onDisconnect");if(port == mPort){
mPort =null;}}};/**
* 向 js 发送数据 示例:evaluateJavascript("callStartUpload", "startUpload");
*
* @param methodName 定义的方法名
* @param data 发送的数据
*/privatevoidevaluateJavascript(String methodName,String data){try{long id =System.currentTimeMillis();JSONObject message =newJSONObject();
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){thrownewRuntimeException(ex);}}/**
* web 端:
*
* 接收消息示例:window.callStartUpload = function(data){console.log(data)}
*
* 发送消息示例:
* if(typeof window.JSBridge !== 'undefined'){
* window.JSBridge.postMessage({function:name, args})
* }
*
*/privateintgetViewPadding(finalAlertDialog.Builder builder){finalTypedArray attr =
builder
.getContext().obtainStyledAttributes(newint[]{android.R.attr.listPreferredItemPaddingLeft});finalint padding = attr.getDimensionPixelSize(0,1);
attr.recycle();return padding;}privateLinearLayoutaddStandardLayout(finalAlertDialog.Builder builder,finalString title,finalString msg){finalScrollView scrollView =newScrollView(builder.getContext());finalLinearLayout container =newLinearLayout(builder.getContext());finalint horizontalPadding =getViewPadding(builder);finalint 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;}privateSpinneraddMediaSpinner(finalContext context,finalViewGroup container,finalGeckoSession.PermissionDelegate.MediaSource[] sources,finalString[] sourceNames){finalArrayAdapter<GeckoSession.PermissionDelegate.MediaSource> adapter =newArrayAdapter<GeckoSession.PermissionDelegate.MediaSource>(context,android.R.layout.simple_spinner_item){privateViewconvertView(finalint position,finalView view){if(view !=null){finalGeckoSession.PermissionDelegate.MediaSource item =getItem(position);((TextView) view).setText(sourceNames !=null? sourceNames[position]: item.name);}return view;}@OverridepublicViewgetView(finalint position,View view,finalViewGroup parent){returnconvertView(position,super.getView(position, view, parent));}@OverridepublicViewgetDropDownView(finalint position,finalView view,finalViewGroup parent){returnconvertView(position,super.getDropDownView(position, view, parent));}};
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
adapter.addAll(sources);finalSpinner spinner =newSpinner(context);
spinner.setAdapter(adapter);
spinner.setSelection(0);
container.addView(spinner);return spinner;}privateString[]normalizeMediaName(finalGeckoSession.PermissionDelegate.MediaSource[] sources){if(sources ==null){returnnull;}String[] res =newString[sources.length];for(int i =0; i < sources.length; i++){finalint mediaSource = sources[i].source;finalString 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);}}elseif(!name.isEmpty()){
res[i]= name;}elseif(GeckoSession.PermissionDelegate.MediaSource.SOURCE_MICROPHONE== mediaSource){
res[i]=getString(R.string.media_microphone);}else{
res[i]=getString(R.string.media_other);}}return res;}@OverridepublicvoidonRequestPermissionsResult(int requestCode,@NonNullString[] permissions,@NonNullint[] 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();}}}@OverrideprotectedvoidonDestroy(){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);}returnPromise.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();returnPromise.resolve(evalCallBack);}returnPromise.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":"[email protected]"}},"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>
版权归原作者 熱情 所有, 如有侵权,请联系我们删除。