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

avatar
作者
猴君
阅读量: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> 

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!