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

news2024/11/24 17:38:22

参考文档:

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

        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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1626331.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL中的死锁预防和解决

MySQL中的死锁预防和解决 死锁是数据库管理系统中常见的问题&#xff0c;特别是在高并发的应用场景下。MySQL数据库中的死锁会导致事务处理速度减慢&#xff0c;甚至完全停止&#xff0c;因此理解并预防死锁至关重要。本文将详细介绍如何预防MySQL中的死锁&#xff0c;包括常用…

【算法基础实验】图论-深度优先搜索和深度优先路径

深度优先(DFS) 理论基础 深度优先搜索&#xff08;DFS, Depth-First Search&#xff09;是图和树的遍历算法中的一种&#xff0c;它从一个节点开始&#xff0c;沿着树的边走到尽可能深的分支&#xff0c;直到节点没有子节点为止&#xff0c;然后回溯继续搜索下一个分支。DFS …

网络安全实训Day17and18

写在前面 第17和18天都讲的sql注入&#xff0c;故合并 ​​​​​​ 网络空间安全实训-渗透测试 Web渗透 定义 针对Web站点的渗透攻击&#xff0c;以获取网站控制权限为目的 Web渗透的特点 Web技术学习门槛低&#xff0c;更容易实现 Web的普及性决定了Web渗透更容易找到目…

JavaEE 初阶篇-深入了解 I/O 高级流(缓冲流、交换流、数据流和序列化流)

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 缓冲流概述 1.1 缓冲流的工作原理 1.2 使用缓冲流的步骤 1.3 字节缓冲流于字符缓冲流的区别 1.4 字节缓冲流的实例 1.5 字符缓冲流的实例 2.0 转换流概述 2.1 字符…

MySQL函数之单行函数

1.前言 我们在使用 SQL 语言的时候&#xff0c;不是直接和这门语言打交道&#xff0c;而是通过它使用不同的数据库软件&#xff0c;即DBMS。DBMS 之间的差异性很大&#xff0c;远大于同一个语言不同版本之间的差异。实际上&#xff0c;只有很少的函数是被 DBMS 同时支持的。比…

MySQL基础知识——MySQL索引

深入浅出索引 索引的意义 索引的意义&#xff1a;在大量数据中&#xff0c;加速访问少量特定数据&#xff1b; 使用索引的前提条件&#xff1a; 1&#xff09;索引块数量小于数据块数量&#xff1b; 2&#xff09;索引键有序&#xff0c;故可以使用二分查找等高效的查找方式&…

go语言并发实战——日志收集系统(十) 重构tailfile模块实现同时监控多个日志文件

前言 在上一篇文章中&#xff0c;我们实现了通过etcd来同时指定多个不同的有关分区与日志文件的路径&#xff0c;但是锁着一次读取配置的增多&#xff0c;不可避免的出现了一个问题&#xff1a;我们如何来监控多个日志文件&#xff0c;这样原来的tailFile模块相对于当下场景就…

前端到全栈进阶之“前端框架”

从前端入门到全栈-系列介绍 你会学到什么&#xff1f; 可能学不到什么东西&#xff0c;该系列是作者本人工作和学习积累&#xff0c;用于复习 系列介绍 现在的 Web 前端已经离不开 Node.js&#xff0c;我们广泛使用的 Babel、Webpack、工程化都是基于 Node 的&#xff0c;各…

【Linux】驱动_2_字符驱动

1. Linux设备分类 字符设备: 指应用程序按字节/字符来读写数据的设备。通常为传真、虚拟终端和串口调制解调器、键盘之类设备提供流通信服务&#xff0c;通常不支持随机存取数据。字符设备在实现时大多不使用缓存器。系统直接从设备读/写每一个字符。块设备: 通常支持随机存取…

【程序分享1】LAMMPS + OVITO + 晶体缺陷识别 + 点缺陷 + 分子动力学模拟

分享2个分子动力学模拟相关的程序。 1. 一种识别体心立方晶体缺陷的新方法。 2. 无后处理的分子动力学模拟中的并行点缺陷识别: lammps的计算和转储方式 。 感谢论文的原作者&#xff01; 第1个程序 关键词&#xff1a; 1. Atomistic simulations, 2. Molecular dynamics…

让客服工作开挂的8个客服办公高效率神器

做客服工作&#xff0c;经常需要写文案&#xff0c;做图片做视频&#xff0c;还要能快捷回复客户&#xff0c;都需要有靠谱的客服办公软件支持&#xff0c;本文介绍了8个高效神器&#xff0c;希望能帮到做客服的亲 前言 做客服工作&#xff0c;在回答客户咨询的同时&#xff0…

2024.4.28 机器学习周报

目录 引言 Abstract 文献阅读 1、题目 2、引言 3、创新点 4、总体流程 5、网络结构 5.1、损失函数 5.2、Confidence Maps 5.3、Part Affinity Fields(PAFs) 5.4、多人的PAFs 6、实验 7、结论 深度学习 yolov8实现目标检测和人体姿态估计 Yolov8网络结构 yaml…

【亲测可用】配置镜像源

文章目录 配置镜像源1. 手动添加镜像源2. 永久配置&#xff08;推荐&#xff09;方法1&#xff1a;方法2 &#xff1a; 小结 配置镜像源 配置镜像源会让资源下载的更快一些 我实验了一下&#xff0c;都成功了的方法&#xff0c;推荐给你们 1.手动添加 2.永久配置 前提是你的…

好看到爆炸的弹窗公告源码

源码介绍 好看到爆炸的弹窗公告源码&#xff0c;源码由HTMLCSSJS组成&#xff0c;记事本打开源码文件可以进行内容文字之类的修改&#xff0c;双击html文件可以本地运行效果&#xff0c;也可以上传到服务器里面&#xff0c; 源码截图 源码下载 好看到爆炸的弹窗公告源码

新标准日本语初下 课后练习作业

新版标准日本语初下 第二十五課 これは明日会議で使う資料です 第二十五課 これは明日会議で使う資料です &#xff12;&#xff14;&#xff0d;&#xff10;&#xff14;&#xff0d;&#xff12;&#xff16; 練習&#xff12;&#xff15;&#xff0d;1&#xff0d;1 例…

Vuforia AR篇(四)— AR虚拟按钮

目录 前言一、创建虚拟按钮二、创建脚本三、效果 前言 在当今互联网和移动设备普及的背景下&#xff0c;**增强现实&#xff08;AR&#xff09;**技术正迅速成为连接现实世界与数字信息的重要桥梁。AR虚拟按钮作为这一技术的创新应用&#xff0c;不仅提供了一种全新的用户交互…

[极客大挑战 2019]Upload、[ACTF2020 新生赛]Upload、[MRCTF2020]你传你呢

[极客大挑战 2019]Upload 打开环境&#xff0c;是上传一句话木马的题 先上传1.php试试&#xff0c;发现不可以 试试改后缀为phtml&#xff0c;提示语句中不能包含<?&#xff0c;只能改木马&#xff1a; <script language"php">eval($_POST[line]);</sc…

ListView、RecycleView、动画、单位、ViewPager

ListView列表 老版本 public View oldGetView(int position, View convertView, ViewGroup parent) {//返回每一个item//拿到布局if (convertViewnull)convertView LayoutInflater.from(context).inflate(R.layout.my_list_view, parent, false);//find会耗时需要优化TextVi…

YOLOv8核心原理深度解析

YOLOv8源码地址: https://github.com/ultralytics/ultralytics 一、简介: 根据官方描述,Yolov8是一个SOTA模型,它建立在Yolo系列历史版本的基础上,并引入了新的功能和改进点,以进一步提升性能和灵活性,使其成为实现目标检测、图像分割、姿态估计等任务的最佳选择。其具体…

Rancher-Longhorn-新增磁盘以及卷创建原理和卷副本调度规则

一、添加磁盘-官网指引 重点在于&#xff1a; 1、比如你新增了一块盘&#xff0c;你需要做一下事情&#xff1a; 1、执行 lsblk 能找到你的盘。 2、然后执行 fdisk /dev/sdxx 分区你的盘。 3、然后对于分区部署文件系统&#xff0c; mkfs.xfs 4、然后执行 mount /dev/sdxxx 你…