小工具知识网

当前位置: 首页 > 行业知识 > Android 使用 GeckoView 并实现 js 交互、权限交互

行业知识

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

2026-11-09 08:27:43 来源:互联网转载

参考文档:

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();            }        });        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  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 messageport.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 messagebrowser.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

                @drawable/web_view_progress        2dp        2dp    

web_view_progress

                                                                                                                                

colors.xml

    #FF2673FF

strings.xml

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

上一篇:手机做中继服务器怎么设置?

下一篇:怎么开私人服务器玩?