Android 使用 GeckoView 并实现 js 交互、权限交互
创始人
2025-01-15 19:36:22
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' 

使用方式:

控件:

                

初始化及配置

 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 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 adapter =                 new ArrayAdapter(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": [         ""       ],       "js": [         "content.js"       ],       "run_at": "document_start"     }   ],   "background": {     "scripts": [       "background.js"     ]   },   "permissions": [     "nativeMessaging",     "nativeMessagingFromContent",     "geckoViewAddons",     "webNavigation",     "geckoview",     "tabs",     ""   ],   "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'" } 

其他资源文件:

themes.xml

          

web_view_progress

                                                                                                                                                       

colors.xml

    #FF2673FF 

strings.xml

    麦克风打开     摄像头打开     摄像头和麦克风打开      背面摄像头     前置摄像头     麦克风     未知来源      与共享视频 "%1$s"     与共享音频 "%1$s"     与共享视频和音频 "%1$s" 

相关内容

热门资讯

必备辅助挂wpk微扑克模拟器!... 您好,wpk微扑克模拟器这款游戏可以开挂的,确实是有挂的,需要了解加微【439369440】很多玩家...
攻略讲解(竞技德州联盟扑克)软... 攻略讲解(竞技德州联盟扑克)软件透明挂(辅助挂)透明挂软件(2022已更新)(哔哩哔哩)是一款可以让...
一分钟了解(WPK机器人)软件... 一分钟了解(WPK机器人)软件透明挂(透视)外挂辅助器神器(2022已更新)(哔哩哔哩)是一款可以让...
推荐几款新版蛮籽重庆麻将有挂的... 推荐几款新版蛮籽重庆麻将有挂的!太离谱了其实是真的有挂(2024已更新)(有挂测试);蛮籽重庆麻将有...
第八辅助挂(WPK打法)软件透... 您好,WPK打法这款游戏可以开挂的,确实是有挂的,需要了解加微【136704302】很多玩家在这款游...
5分钟了解八闽13水有外挂的!... 5分钟了解八闽13水有外挂的!太夸张了其实是真的有挂(2021已更新)(有挂来袭);1、这是跨平台的...
一分钟教你(wpk)软件透明挂... 一分钟教你(wpk)软件透明挂(辅助挂)透明挂插件(2020已更新)(哔哩哔哩);相信小伙伴都知道这...
科技新动态聚乐手机麻将助赢神器... 科技新动态聚乐手机麻将助赢神器!太离谱了原来是有挂(2021已更新)(有挂教程)是一款可以让一直输的...
玩家必看秘籍(Wepoke中牌... 玩家必看秘籍(Wepoke中牌率)软件透明挂(透视)外挂辅助器插件(2025已更新)(哔哩哔哩);最...
终于懂了科乐麻将系统规律!太夸... 【福星临门,好运相随】;终于懂了科乐麻将系统规律!太夸张了其实确实是真的有挂(2020已更新)(有挂...