ToB企服应用市场:ToB评测及商务社交产业平台

标题: Android 使用 GeckoView 并实现 js 交互、权限交互 [打印本页]

作者: 海哥    时间: 2024-9-20 16:40
标题: Android 使用 GeckoView 并实现 js 交互、权限交互
参考文档:
geckoview版本
引入文档(有坑 下面会给出正确引入方式)
官方示例代码1
官方示例代码2
参考了两位大神的博客和demo:
GeckoView js交互实现
geckoview-jsdemo
引入方式:

  1.         maven {
  2.             url "https://maven.mozilla.org/maven2/"
  3.         }
复制代码
  1.     compileOptions {
  2.         sourceCompatibility JavaVersion.VERSION_1_8
  3.         targetCompatibility JavaVersion.VERSION_1_8
  4.     }
复制代码
  1. implementation 'org.mozilla.geckoview:geckoview-arm64-v8a:111.0.20230309232128'
复制代码
使用方式:

控件:
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3.     xmlns:tools="http://schemas.android.com/tools"
  4.     android:layout_width="match_parent"
  5.     android:layout_height="match_parent"
  6.     android:orientation="vertical">
  7.     <org.mozilla.geckoview.GeckoView
  8.         android:id="@+id/web_view"
  9.         android:layout_width="match_parent"
  10.         android:layout_height="match_parent" />
  11.     <ProgressBar
  12.         android:id="@+id/web_progress"
  13.         style="@style/Web.ProgressBar.Horizontal"
  14.         android:layout_width="match_parent"
  15.         android:layout_height="wrap_content"
  16.         android:visibility="visible"
  17.         tools:progress="50" />
  18. </RelativeLayout>
复制代码
初始化及设置
  1. import androidx.annotation.NonNull;
  2. import androidx.annotation.Nullable;
  3. import androidx.appcompat.app.AlertDialog;
  4. import androidx.appcompat.app.AppCompatActivity;
  5. import androidx.core.app.ActivityCompat;
  6. import androidx.core.content.ContextCompat;
  7. import android.Manifest;
  8. import android.content.Context;
  9. import android.content.DialogInterface;
  10. import android.content.pm.PackageManager;
  11. import android.content.res.TypedArray;
  12. import android.net.Uri;
  13. import android.os.Bundle;
  14. import android.text.TextUtils;
  15. import android.util.Log;
  16. import android.view.View;
  17. import android.view.ViewGroup;
  18. import android.widget.ArrayAdapter;
  19. import android.widget.LinearLayout;
  20. import android.widget.ProgressBar;
  21. import android.widget.ScrollView;
  22. import android.widget.Spinner;
  23. import android.widget.TextView;
  24. import org.json.JSONException;
  25. import org.json.JSONObject;
  26. import org.mozilla.geckoview.GeckoResult;
  27. import org.mozilla.geckoview.GeckoRuntime;
  28. import org.mozilla.geckoview.GeckoRuntimeSettings;
  29. import org.mozilla.geckoview.GeckoSession;
  30. import org.mozilla.geckoview.GeckoSessionSettings;
  31. import org.mozilla.geckoview.GeckoView;
  32. import org.mozilla.geckoview.WebExtension;
  33. import java.util.Locale;
  34. public class MainActivity extends AppCompatActivity {
  35.     private static final String TAG = "MainActivityTag";
  36.     // 权限回调码
  37.     private static final int CAMERA_PERMISSION_REQUEST_CODE = 1000;
  38.     // web - 测试环境
  39.     private static final String WEB_URL = "https://xxx.xxx.com/";
  40.     private static final String EXTENSION_LOCATION = "resource://android/assets/messaging/";
  41.     private static final String EXTENSION_ID = "messaging@example.com";
  42.     private static GeckoRuntime sRuntime = null;
  43.     private GeckoSession session;
  44.     private static WebExtension.Port mPort;
  45.     private GeckoSession.PermissionDelegate.Callback mCallback;
  46.     @Override
  47.     protected void onCreate(Bundle savedInstanceState) {
  48.         super.onCreate(savedInstanceState);
  49.         setContentView(R.layout.activity_main);
  50.         setupGeckoView();
  51.     }
  52.     private void setupGeckoView() {
  53.         // 初始化控件
  54.         GeckoView geckoView = findViewById(R.id.gecko_view);
  55.         ProgressBar web_progress = findViewById(R.id.web_progress);
  56.         if (sRuntime == null) {
  57.             GeckoRuntimeSettings.Builder builder = new GeckoRuntimeSettings.Builder()
  58.                     .allowInsecureConnections(GeckoRuntimeSettings.ALLOW_ALL)
  59.                     .javaScriptEnabled(true)
  60.                     .doubleTapZoomingEnabled(true)
  61.                     .inputAutoZoomEnabled(true)
  62.                     .forceUserScalableEnabled(true)
  63.                     .aboutConfigEnabled(true)
  64.                     .loginAutofillEnabled(true)
  65.                     .webManifest(true)
  66.                     .consoleOutput(true)
  67.                     .remoteDebuggingEnabled(BuildConfig.DEBUG)
  68.                     .debugLogging(BuildConfig.DEBUG);
  69.             sRuntime = GeckoRuntime.create(this, builder.build());
  70.         }
  71.         // 建立交互
  72.         installExtension();
  73.         session = new GeckoSession();
  74.         GeckoSessionSettings settings = session.getSettings();
  75.         settings.setAllowJavascript(true);
  76.         settings.setUserAgentMode(GeckoSessionSettings.USER_AGENT_MODE_MOBILE);
  77.         session.getPanZoomController().setIsLongpressEnabled(false);
  78.         // 监听网页加载进度
  79.         session.setProgressDelegate(new GeckoSession.ProgressDelegate() {
  80.             @Override
  81.             public void onPageStart(GeckoSession session, String url) {
  82.                 // 网页开始加载时的操作
  83.                 if (web_progress != null) {
  84.                     web_progress.setVisibility(View.VISIBLE);
  85.                 }
  86.             }
  87.             @Override
  88.             public void onPageStop(GeckoSession session, boolean success) {
  89.                 // 网页加载完成时的操作
  90.                 if (web_progress != null) {
  91.                     web_progress.setVisibility(View.GONE);
  92.                 }
  93.             }
  94.             @Override
  95.             public void onProgressChange(GeckoSession session, int progress) {
  96.                 // 网页加载进度变化时的操作
  97.                 if (web_progress != null) {
  98.                     web_progress.setProgress(progress);
  99.                 }
  100.             }
  101.         });
  102.         // 权限
  103.         session.setPermissionDelegate(new GeckoSession.PermissionDelegate() {
  104.             @Override
  105.             public void onAndroidPermissionsRequest(@NonNull final GeckoSession session,
  106.                                                     final String[] permissions,
  107.                                                     @NonNull final Callback callback) {
  108.                 mCallback = callback;
  109.                 if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
  110.                         || ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
  111.                     ActivityCompat.requestPermissions(MainActivity.this, permissions, CAMERA_PERMISSION_REQUEST_CODE);
  112.                 } else {
  113.                     callback.grant();
  114.                 }
  115.             }
  116.             @Nullable
  117.             @Override
  118.             public GeckoResult<Integer> onContentPermissionRequest(@NonNull GeckoSession session, @NonNull ContentPermission perm) {
  119.                 return GeckoResult.fromValue(ContentPermission.VALUE_ALLOW);
  120.             }
  121.             @Override
  122.             public void onMediaPermissionRequest(@NonNull final GeckoSession session,
  123.                                                  @NonNull final String uri,
  124.                                                  final MediaSource[] video,
  125.                                                  final MediaSource[] audio,
  126.                                                  @NonNull final MediaCallback callback) {
  127.                 final String host = Uri.parse(uri).getAuthority();
  128.                 final String title;
  129.                 if (audio == null) {
  130.                     title = getString(R.string.request_video, host);
  131.                 } else if (video == null) {
  132.                     title = getString(R.string.request_audio, host);
  133.                 } else {
  134.                     title = getString(R.string.request_media, host);
  135.                 }
  136.                 String[] videoNames = normalizeMediaName(video);
  137.                 String[] audioNames = normalizeMediaName(audio);
  138.                 final AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
  139.                 final LinearLayout container = addStandardLayout(builder, title, null);
  140.                 final Spinner videoSpinner;
  141.                 if (video != null) {
  142.                     videoSpinner = addMediaSpinner(builder.getContext(), container, video, videoNames); // create spinner and add to alert UI
  143.                 } else {
  144.                     videoSpinner = null;
  145.                 }
  146.                 final Spinner audioSpinner;
  147.                 if (audio != null) {
  148.                     audioSpinner = addMediaSpinner(builder.getContext(), container, audio, audioNames); // create spinner and add to alert UI
  149.                 } else {
  150.                     audioSpinner = null;
  151.                 }
  152. // 手动同意权限
  153.                 builder.setNegativeButton(android.R.string.cancel, null)
  154.                         .setPositiveButton(android.R.string.ok,
  155.                                 new DialogInterface.OnClickListener() {
  156.                                     @Override
  157.                                     public void onClick(final DialogInterface dialog, final int which) {
  158.                                         // gather selected media devices and grant access
  159.                                         final MediaSource video = (videoSpinner != null)
  160.                                                 ? (MediaSource) videoSpinner.getSelectedItem() : null;
  161.                                         final MediaSource audio = (audioSpinner != null)
  162.                                                 ? (MediaSource) audioSpinner.getSelectedItem() : null;
  163.                                         callback.grant(video, audio);
  164.                                     }
  165.                                 });
  166.                 final AlertDialog dialog = builder.create();
  167.                 dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
  168.                     @Override
  169.                     public void onDismiss(final DialogInterface dialog) {
  170.                         callback.reject();
  171.                     }
  172.                 });
  173.                 dialog.show();
  174.         // 自动同意权限
  175.         /*
  176.                 final MediaSource videoMediaSource = (videoSpinner != null)
  177.                         ? (MediaSource) videoSpinner.getSelectedItem() : null;
  178.                 final MediaSource audioMediaSource = (audioSpinner != null)
  179.                         ? (MediaSource) audioSpinner.getSelectedItem() : null;
  180.                 callback.grant(videoMediaSource, audioMediaSource);
  181.         */
  182.             }
  183.         });
  184.         session.open(sRuntime);
  185.         geckoView.setSession(session);
  186.         // 打开web地址
  187.         session.loadUri(WEB_URL);
  188.     }
  189.     /**
  190.      * 建立交互
  191.      */
  192.     private void installExtension() {
  193.         sRuntime.getWebExtensionController()
  194.                 .ensureBuiltIn(EXTENSION_LOCATION, EXTENSION_ID)
  195.                 .accept(
  196.                         extension -> {
  197.                             Log.i(TAG, "Extension installed: " + extension);
  198.                             runOnUiThread(() -> {
  199.                                 assert extension != null;
  200.                                 extension.setMessageDelegate(mMessagingDelegate, "Android");
  201.                             });
  202.                         },
  203.                         e -> Log.e(TAG, "Error registering WebExtension", e)
  204.                 );
  205.     }
  206.     private final WebExtension.MessageDelegate mMessagingDelegate = new WebExtension.MessageDelegate() {
  207.         @Nullable
  208.         @Override
  209.         public void onConnect(@NonNull WebExtension.Port port) {
  210.             Log.e(TAG, "MessageDelegate onConnect");
  211.             mPort = port;
  212.             mPort.setDelegate(mPortDelegate);
  213.         }
  214.     };
  215.     /**
  216.      * 接收 JS 发送的消息
  217.      */
  218.     private final WebExtension.PortDelegate mPortDelegate = new WebExtension.PortDelegate() {
  219.         @Override
  220.         public void onPortMessage(final @NonNull Object message,
  221.                                   final @NonNull WebExtension.Port port) {
  222.             Log.e(TAG, "from extension: " + message);
  223.             try {
  224. //                ToastUtils.showLong("收到js调用: " + message);
  225.                 if (message instanceof JSONObject) {
  226.                     JSONObject jsonobject = (JSONObject) message;
  227.                     /*
  228.                      * jsonobject 格式
  229.                      *
  230.                      *  {
  231.                      *    "action": "JSBridge",
  232.                      *    "data": {
  233.                      *          "args":"字符串",
  234.                      *          "function":"方法名"
  235.                      *    }
  236.                      *  }
  237.                      */
  238.                     String action = jsonobject.getString("action");
  239.                     if ("JSBridge".equals(action)) {
  240.                         JSONObject data = jsonobject.getJSONObject("data");
  241.                         String function = data.getString("function");
  242.                         if (!TextUtils.isEmpty(function)) {
  243.                             String args = data.getString("args");
  244.                             switch (function) {
  245.                                 // 与前端定义的方法名 示例:callSetToken
  246.                                 case "callSetToken": {
  247.                                     break;
  248.                                 }
  249.                             }
  250.                         }
  251.                     }
  252.                 }
  253.             } catch (Exception e) {
  254.                 e.printStackTrace();
  255.             }
  256.         }
  257.         @Override
  258.         public void onDisconnect(final @NonNull WebExtension.Port port) {
  259.             Log.e(TAG, "MessageDelegate:onDisconnect");
  260.             if (port == mPort) {
  261.                 mPort = null;
  262.             }
  263.         }
  264.     };
  265.     /**
  266.      * 向 js 发送数据 示例:evaluateJavascript("callStartUpload", "startUpload");
  267.      *
  268.      * @param methodName 定义的方法名
  269.      * @param data       发送的数据
  270.      */
  271.     private void evaluateJavascript(String methodName, String data) {
  272.         try {
  273.             long id = System.currentTimeMillis();
  274.             JSONObject message = new JSONObject();
  275.             message.put("action", "evalJavascript");
  276.             message.put("data", "window." + methodName + "('" + data + "')");
  277.             message.put("id", id);
  278.             mPort.postMessage(message);
  279.             Log.e(TAG,"mPort.postMessage:" + message);
  280.         } catch (JSONException ex) {
  281.             throw new RuntimeException(ex);
  282.         }
  283.     }
  284.    
  285.     /**
  286.      * web 端:
  287.      *
  288.      * 接收消息示例:window.callStartUpload = function(data){console.log(data)}
  289.      *
  290.      * 发送消息示例:
  291.      * if(typeof window.JSBridge !== 'undefined'){
  292.      *   window.JSBridge.postMessage({function:name, args})
  293.      * }
  294.      *
  295.      */
  296.     private int getViewPadding(final AlertDialog.Builder builder) {
  297.         final TypedArray attr =
  298.                 builder
  299.                         .getContext()
  300.                         .obtainStyledAttributes(new int[]{android.R.attr.listPreferredItemPaddingLeft});
  301.         final int padding = attr.getDimensionPixelSize(0, 1);
  302.         attr.recycle();
  303.         return padding;
  304.     }
  305.     private LinearLayout addStandardLayout(
  306.             final AlertDialog.Builder builder, final String title, final String msg) {
  307.         final ScrollView scrollView = new ScrollView(builder.getContext());
  308.         final LinearLayout container = new LinearLayout(builder.getContext());
  309.         final int horizontalPadding = getViewPadding(builder);
  310.         final int verticalPadding = (msg == null || msg.isEmpty()) ? horizontalPadding : 0;
  311.         container.setOrientation(LinearLayout.VERTICAL);
  312.         container.setPadding(
  313.                 /* left */ horizontalPadding, /* top */ verticalPadding,
  314.                 /* right */ horizontalPadding, /* bottom */ verticalPadding);
  315.         scrollView.addView(container);
  316.         builder.setTitle(title).setMessage(msg).setView(scrollView);
  317.         return container;
  318.     }
  319.     private Spinner addMediaSpinner(
  320.             final Context context,
  321.             final ViewGroup container,
  322.             final GeckoSession.PermissionDelegate.MediaSource[] sources,
  323.             final String[] sourceNames) {
  324.         final ArrayAdapter<GeckoSession.PermissionDelegate.MediaSource> adapter =
  325.                 new ArrayAdapter<GeckoSession.PermissionDelegate.MediaSource>(context, android.R.layout.simple_spinner_item) {
  326.                     private View convertView(final int position, final View view) {
  327.                         if (view != null) {
  328.                             final GeckoSession.PermissionDelegate.MediaSource item = getItem(position);
  329.                             ((TextView) view).setText(sourceNames != null ? sourceNames[position] : item.name);
  330.                         }
  331.                         return view;
  332.                     }
  333.                     @Override
  334.                     public View getView(final int position, View view, final ViewGroup parent) {
  335.                         return convertView(position, super.getView(position, view, parent));
  336.                     }
  337.                     @Override
  338.                     public View getDropDownView(final int position, final View view, final ViewGroup parent) {
  339.                         return convertView(position, super.getDropDownView(position, view, parent));
  340.                     }
  341.                 };
  342.         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
  343.         adapter.addAll(sources);
  344.         final Spinner spinner = new Spinner(context);
  345.         spinner.setAdapter(adapter);
  346.         spinner.setSelection(0);
  347.         container.addView(spinner);
  348.         return spinner;
  349.     }
  350.     private String[] normalizeMediaName(final GeckoSession.PermissionDelegate.MediaSource[] sources) {
  351.         if (sources == null) {
  352.             return null;
  353.         }
  354.         String[] res = new String[sources.length];
  355.         for (int i = 0; i < sources.length; i++) {
  356.             final int mediaSource = sources[i].source;
  357.             final String name = sources[i].name;
  358.             if (GeckoSession.PermissionDelegate.MediaSource.SOURCE_CAMERA == mediaSource) {
  359.                 if (name.toLowerCase(Locale.ROOT).contains("front")) {
  360.                     res[i] = getString(R.string.media_front_camera);
  361.                 } else {
  362.                     res[i] = getString(R.string.media_back_camera);
  363.                 }
  364.             } else if (!name.isEmpty()) {
  365.                 res[i] = name;
  366.             } else if (GeckoSession.PermissionDelegate.MediaSource.SOURCE_MICROPHONE == mediaSource) {
  367.                 res[i] = getString(R.string.media_microphone);
  368.             } else {
  369.                 res[i] = getString(R.string.media_other);
  370.             }
  371.         }
  372.         return res;
  373.     }
  374.     @Override
  375.     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
  376.         super.onRequestPermissionsResult(requestCode, permissions, grantResults);
  377.         if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
  378.             if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  379.                 // 授予权限
  380.                 mCallback.grant();
  381.             } else {
  382.                 // 拒绝权限
  383.                 mCallback.reject();
  384.             }
  385.         }
  386.     }
  387.     @Override
  388.     protected void onDestroy() {
  389.         super.onDestroy();
  390.         if (session != null) {
  391.             session.close();
  392.         }
  393.     }
  394. }
复制代码
资源文件设置:
在assets下新建:messaging 文件夹

.eslintrc.js
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. module.exports = {
  6.   env: {
  7.     webextensions: true,
  8.   },
  9. };
复制代码
background.js
  1. // Establish connection with app
  2. 'use strict';
  3. const port = browser.runtime.connectNative("Android");
  4. async function sendMessageToTab(message) {
  5. try {
  6.    let tabs = await browser.tabs.query({})
  7.    console.log(`background:tabs:${tabs}`)
  8.    return await browser.tabs.sendMessage(
  9.      tabs[tabs.length - 1].id,
  10.      message
  11.    )
  12. } catch (e) {
  13.    console.log(`background:sendMessageToTab:req:error:${e}`)
  14.    return e.toString();
  15. }
  16. }
  17. //监听 app message
  18. port.onMessage.addListener(request => {
  19. let action = request.action;
  20. if(action === "evalJavascript") {
  21.      sendMessageToTab(request).then((resp) => {
  22.        port.postMessage(resp);
  23.      }).catch((e) => {
  24.        console.log(`background:sendMessageToTab:resp:error:${e}`)
  25.      });
  26.    }
  27. })
  28. //接收 content.js message
  29. browser.runtime.onMessage.addListener((data, sender) => {
  30.    let action = data.action;
  31.    console.log("background:content:onMessage:" + action);
  32.    if (action === 'JSBridge') {
  33.        port.postMessage(data);
  34.    }
  35.    return Promise.resolve('done');
  36. })
复制代码
content.js
  1. console.log(`content:start`);
  2. let JSBridge = {
  3.     postMessage: function (message) {
  4.         browser.runtime.sendMessage({
  5.             action: "JSBridge",
  6.             data: message
  7.         });
  8.     }
  9. }
  10. window.wrappedJSObject.JSBridge = cloneInto(
  11.     JSBridge,
  12.     window,
  13.     { cloneFunctions: true });
  14. browser.runtime.onMessage.addListener((data, sender) => {
  15.     console.log("content:eval:" + data);
  16.     if (data.action === 'evalJavascript') {
  17.         let evalCallBack = {
  18.             id: data.id,
  19.             action: "evalJavascript",
  20.         }
  21.         try {
  22.             let result = window.eval(data.data);
  23.             console.log("content:eval:result" + result);
  24.             if (result) {
  25.                 evalCallBack.data = result;
  26.             } else {
  27.                 evalCallBack.data = "";
  28.             }
  29.         } catch (e) {
  30.             evalCallBack.data = e.toString();
  31.             return Promise.resolve(evalCallBack);
  32.         }
  33.         return Promise.resolve(evalCallBack);
  34.     }
  35. });
复制代码
manifest.json
  1. {
  2.   "manifest_version": 2,
  3.   "name": "messaging",
  4.   "description": "Uses the proxy API to block requests to specific hosts.",
  5.   "version": "3.0",
  6.   "browser_specific_settings": {
  7.     "gecko": {
  8.       "strict_min_version": "65.0",
  9.       "id": "messaging@example.com"
  10.     }
  11.   },
  12.   "content_scripts": [
  13.     {
  14.       "matches": [
  15.         "<all_urls>"
  16.       ],
  17.       "js": [
  18.         "content.js"
  19.       ],
  20.       "run_at": "document_start"
  21.     }
  22.   ],
  23.   "background": {
  24.     "scripts": [
  25.       "background.js"
  26.     ]
  27.   },
  28.   "permissions": [
  29.     "nativeMessaging",
  30.     "nativeMessagingFromContent",
  31.     "geckoViewAddons",
  32.     "webNavigation",
  33.     "geckoview",
  34.     "tabs",
  35.     "<all_urls>"
  36.   ],
  37.   "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
  38. }
复制代码
其他资源文件:
themes.xml
  1.     <!-- WebView进度条 -->
  2.     <style name="Web.ProgressBar.Horizontal" parent="@android:style/Widget.ProgressBar.Horizontal">
  3.         <item name="android:progressDrawable">@drawable/web_view_progress</item>
  4.         <item name="android:minHeight">2dp</item>
  5.         <item name="android:maxHeight">2dp</item>
  6.     </style>
复制代码
web_view_progress
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
  3.     <item android:id="@android:id/background">
  4.         <shape>
  5.             <corners android:radius="0dp" />
  6.             <gradient
  7.                 android:angle="270"
  8.                 android:centerY="0.75"
  9.                 android:endColor="#A0B3CF"
  10.                 android:startColor="#A0B3CF" />
  11.         </shape>
  12.     </item>
  13.     <item android:id="@android:id/progress">
  14.         <clip>
  15.             <shape>
  16.                 <corners android:radius="0dp" />
  17.                 <gradient
  18.                     android:angle="270"
  19.                     android:endColor="@color/colorPrimary"
  20.                     android:startColor="@color/colorPrimary" />
  21.             </shape>
  22.         </clip>
  23.     </item>
  24. </layer-list>
复制代码
colors.xml
  1.     <color name="colorPrimary">#FF2673FF</color>
复制代码
strings.xml
  1.     <string name="device_sharing_microphone">麦克风打开</string>
  2.     <string name="device_sharing_camera">摄像头打开</string>
  3.     <string name="device_sharing_camera_and_mic">摄像头和麦克风打开</string>
  4.     <string name="media_back_camera">背面摄像头</string>
  5.     <string name="media_front_camera">前置摄像头</string>
  6.     <string name="media_microphone">麦克风</string>
  7.     <string name="media_other">未知来源</string>
  8.     <string name="request_video">与共享视频 "%1$s"</string>
  9.     <string name="request_audio">与共享音频 "%1$s"</string>
  10.     <string name="request_media">与共享视频和音频 "%1$s"</string>
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4