0


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

参考文档:

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>

本文转载自: https://blog.csdn.net/a506656675/article/details/137961354
版权归原作者 熱情 所有, 如有侵权,请联系我们删除。

“Android 使用 GeckoView 并实现 js 交互、权限交互”的评论:

还没有评论