Android 调用系统服务接口获取屏幕投影(需要android.uid.system)

在这里插入图片描述

媒体投影

借助 Android 5(API 级别 21)中引入的 android.media.projection API,您可以将设备屏幕中的内容截取为可播放、录制或投屏到其他设备(如电视)的媒体流。

Android 14(API 级别 34)引入了应用屏幕共享功能,让用户能够分享单个应用窗口(而非整个设备屏幕),无论窗口模式如何。应用屏幕共享功能会将状态栏、导航栏、通知和其他系统界面元素从共享显示屏中排除,即使应用屏幕共享功能用于全屏截取应用也是如此。系统只会分享所选应用的内容。

应用屏幕共享功能可让用户运行多个应用,但仅限于与单个应用共享内容,从而确保用户隐私、提高用户工作效率并增强多任务处理能力。

权限

如果您的应用以 Android 14 或更高版本为目标平台,则应用清单必须包含 mediaProjection 前台服务类型的权限声明:

<manifest ...><uses-permission android:name="android.permission.FOREGROUND_SERVICE" /><uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" /><application ...><serviceandroid:name=".MyMediaProjectionService"android:foregroundServiceType="mediaProjection"android:exported="false"></service></application>
</manifest>

通过调用 startForeground() 启动媒体投影服务。

如果您未在调用中指定前台服务类型,则类型默认为清单中定义的前台服务类型的按位整数。如果清单未指定任何服务类型,系统会抛出 MissingForegroundServiceTypeException

获取MediaProjection示例(常规实现)

AndroidManifest.xml

    <!-- MediaProjection --><uses-permission android:name="android.permission.FOREGROUND_SERVICE" /><uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" /><application><activity android:name=".MediaProjectionTest"/><service android:name=".MediaProjectionService"android:foregroundServiceType="mediaProjection"/></application>

Activity

    MediaProjectionManager projMgr;final int REQUEST_CODE = 0x101;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);projMgr = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);startService(new Intent(this, ForgroundMediaProjectionService.class));startActivityForResult(projMgr.createScreenCaptureIntent(), REQUEST_CODE);}@Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) {if(requestCode == REQUEST_CODE){MediaProjection mp = projMgr.getMediaProjection(resultCode, data);if(mp != null){//mp.stop();//获取到MediaProjection后可以通过MediaCodec编码生成图片/视频/H264流...}}}

Service

	@Overridepublic void onCreate() {super.onCreate();}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {Notification notification = null;Intent activity = new Intent(this, MediaProjectionTest.class);activity.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {NotificationChannel channel = new NotificationChannel("ScreenRecorder", "Foreground notification",NotificationManager.IMPORTANCE_DEFAULT);NotificationManager manager = getSystemService(NotificationManager.class);manager.createNotificationChannel(channel);notification = new Notification.Builder(this, "ScreenRecorder").setContentTitle("Test").setContentText("Test Screencast...").setContentIntent(PendingIntent.getActivity(this, 0x77,activity, PendingIntent.FLAG_UPDATE_CURRENT)).build();}startForeground(1, notification);return super.onStartCommand(intent, flags, startId);}@Overridepublic IBinder onBind(Intent intent) {return null;}

启动Acrtivity后会弹出授权提示
在这里插入图片描述
点击立即开始 Activity.onActivityResult 可以获取到MediaProjection.


如果App是系统应用(android.uid.systtem), 如何跳过授权窗?

  1. 申请MediaProjection过程拆解

涉及源码
frameworks/base/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
frameworks/base/media/java/android/media/projection/MediaProjectionManager.java
frameworks/base/core/res/res/values/config.xml
frameworks/base/packages/SystemUI/AndroidManifest.xml
frameworks/base/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java

函数createScreenCaptureIntent 返回的Intent 指向的是 SystemUI的一个组件:

frameworks/base/media/java/android/media/projection/MediaProjectionManager.java

/*** Returns an Intent that <b>must</b> be passed to startActivityForResult()* in order to start screen capture. The activity will prompt* the user whether to allow screen capture.  The result of this* activity should be passed to getMediaProjection.*/public Intent createScreenCaptureIntent() {Intent i = new Intent();final ComponentName mediaProjectionPermissionDialogComponent =ComponentName.unflattenFromString(mContext.getResources().getString(com.android.internal.R.string.config_mediaProjectionPermissionDialogComponent));i.setComponent(mediaProjectionPermissionDialogComponent);return i;}

frameworks/base/core/res/res/values/config.xml

<string name="config_mediaProjectionPermissionDialogComponent" translatable="false">com.android.systemui/com.android.systemui.media.MediaProjectionPermissionActivity</string>

frameworks/base/packages/SystemUI/AndroidManifest.xml

            <!-- started from MediaProjectionManager --><activityandroid:name=".media.MediaProjectionPermissionActivity"android:exported="true"android:theme="@style/Theme.SystemUI.MediaProjectionAlertDialog"android:finishOnCloseSystemDialogs="true"android:launchMode="singleTop"android:excludeFromRecents="true"android:visibleToInstantApps="true"/>

MediaProjectionPermissionActivity 就是弹窗的主体

frameworks/base/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java

   @Overridepublic void onCreate(Bundle icicle) {super.onCreate(icicle);mPackageName = getCallingPackage();IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);mService = IMediaProjectionManager.Stub.asInterface(b);if (mPackageName == null) {finish();return;}PackageManager packageManager = getPackageManager();ApplicationInfo aInfo;try {aInfo = packageManager.getApplicationInfo(mPackageName, 0);mUid = aInfo.uid;} catch (PackageManager.NameNotFoundException e) {Log.e(TAG, "unable to look up package name", e);finish();return;}try {if (mService.hasProjectionPermission(mUid, mPackageName)) {setResult(RESULT_OK, getMediaProjectionIntent(mUid, mPackageName));finish();return;}} catch (RemoteException e) {Log.e(TAG, "Error checking projection permissions", e);finish();return;}TextPaint paint = new TextPaint();paint.setTextSize(42);CharSequence dialogText = null;CharSequence dialogTitle = null;if (Utils.isHeadlessRemoteDisplayProvider(packageManager, mPackageName)) {dialogText = getString(R.string.media_projection_dialog_service_text);dialogTitle = getString(R.string.media_projection_dialog_service_title);} else {String label = aInfo.loadLabel(packageManager).toString();// If the label contains new line characters it may push the security// message below the fold of the dialog. Labels shouldn't have new line// characters anyways, so just truncate the message the first time one// is seen.final int labelLength = label.length();int offset = 0;while (offset < labelLength) {final int codePoint = label.codePointAt(offset);final int type = Character.getType(codePoint);if (type == Character.LINE_SEPARATOR|| type == Character.CONTROL|| type == Character.PARAGRAPH_SEPARATOR) {label = label.substring(0, offset) + ELLIPSIS;break;}offset += Character.charCount(codePoint);}if (label.isEmpty()) {label = mPackageName;}String unsanitizedAppName = TextUtils.ellipsize(label,paint, MAX_APP_NAME_SIZE_PX, TextUtils.TruncateAt.END).toString();String appName = BidiFormatter.getInstance().unicodeWrap(unsanitizedAppName);String actionText = getString(R.string.media_projection_dialog_text, appName);SpannableString message = new SpannableString(actionText);int appNameIndex = actionText.indexOf(appName);if (appNameIndex >= 0) {message.setSpan(new StyleSpan(Typeface.BOLD),appNameIndex, appNameIndex + appName.length(), 0);}dialogText = message;dialogTitle = getString(R.string.media_projection_dialog_title, appName);}View dialogTitleView = View.inflate(this, R.layout.media_projection_dialog_title, null);TextView titleText = (TextView) dialogTitleView.findViewById(R.id.dialog_title);titleText.setText(dialogTitle);mDialog = new AlertDialog.Builder(this).setCustomTitle(dialogTitleView).setMessage(dialogText).setPositiveButton(R.string.media_projection_action_text, this).setNegativeButton(android.R.string.cancel, this).setOnCancelListener(this).create();mDialog.create();mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true);final Window w = mDialog.getWindow();w.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);w.addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);mDialog.show();}private Intent getMediaProjectionIntent(int uid, String packageName)throws RemoteException {IMediaProjection projection = mService.createProjection(uid, packageName,MediaProjectionManager.TYPE_SCREEN_CAPTURE, false /* permanentGrant */);Intent intent = new Intent();intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, projection.asBinder());return intent;}
  1. 申请成功后返回结果给到申请的Activity:
    getMediaProjectionIntent函数中, 创建了IMediaProjection并通过Intent返回给了调用的App

    setResult(RESULT_OK, getMediaProjectionIntent(mUid, mPackageName));
    
    IMediaProjection projection = mService.createProjection(uid, packageName,MediaProjectionManager.TYPE_SCREEN_CAPTURE, false /* permanentGrant */);Intent intent = new Intent();intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, projection.asBinder());getMediaProjection(resultCode, data)
    
  2. Activity 调用 getMediaProjection 获取MediaProjection

frameworks/base/media/java/android/media/projection/MediaProjectionManager.java

  public MediaProjection getMediaProjection(int resultCode, @NonNull Intent resultData) {if (resultCode != Activity.RESULT_OK || resultData == null) {return null;}IBinder projection = resultData.getIBinderExtra(EXTRA_MEDIA_PROJECTION);if (projection == null) {return null;}return new MediaProjection(mContext, IMediaProjection.Stub.asInterface(projection));}

总的来说, 这个流程稍微绕了一点路:

App MediaProjectionManager SystemUI MediaProjectionService createScreenCaptureIntent MediaProjectionPermissionActivity createProjection onActivityResult getMediaProjection App MediaProjectionManager SystemUI MediaProjectionService

createProjection的实现

frameworks/base/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java

         @Override // Binder callpublic IMediaProjection createProjection(int uid, String packageName, int type,boolean isPermanentGrant) {if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)!= PackageManager.PERMISSION_GRANTED) {throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to grant "+ "projection permission");}if (packageName == null || packageName.isEmpty()) {throw new IllegalArgumentException("package name must not be empty");}final UserHandle callingUser = Binder.getCallingUserHandle();long callingToken = Binder.clearCallingIdentity();MediaProjection projection;try {ApplicationInfo ai;try {ai = mPackageManager.getApplicationInfoAsUser(packageName, 0, callingUser);} catch (NameNotFoundException e) {throw new IllegalArgumentException("No package matching :" + packageName);}projection = new MediaProjection(type, uid, packageName, ai.targetSdkVersion,ai.isPrivilegedApp());if (isPermanentGrant) {mAppOps.setMode(AppOpsManager.OP_PROJECT_MEDIA,projection.uid, projection.packageName, AppOpsManager.MODE_ALLOWED);}} finally {Binder.restoreCallingIdentity(callingToken);}return projection;}

通过反射, 调用MediaProjectionService的createProjection


注意: 此方法需要有系统权限(android.uid.system)

    //android.os.ServiceManager;static Object getService(String name){try {Class ServiceManager = Class.forName("android.os.ServiceManager");Method getService = ServiceManager.getDeclaredMethod("getService", String.class);return getService.invoke(null, name);} catch (ClassNotFoundException e) {e.printStackTrace();} catch (NoSuchMethodException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}return null;}@SuppressLint("SoonBlockedPrivateApi")static Object asInterface(Object binder){try {Class IMediaProjectionManager_Stub = Class.forName("android.media.projection.IMediaProjectionManager$Stub");Method asInterface = IMediaProjectionManager_Stub.getDeclaredMethod("asInterface", IBinder.class);return asInterface.invoke(null, binder);} catch (ClassNotFoundException e) {e.printStackTrace();} catch (NoSuchMethodException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}return null;}//    private IMediaProjectionManager mService;//android.media.projection.IMediaProjectionManager@SuppressLint("SoonBlockedPrivateApi")public static MediaProjection createProjection(){//Context.java public static final String MEDIA_PROJECTION_SERVICE = "media_projection";//IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);//        mService = IMediaProjectionManager.Stub.asInterface(b);IBinder b = (IBinder) getService("media_projection");Object mService = asInterface(b) ;//IMediaProjection projection = mService.createProjection(uid, packageName,//MediaProjectionManager.TYPE_SCREEN_CAPTURE, false /* permanentGrant */);//public static final int TYPE_SCREEN_CAPTURE = 0;try {Logger.i("createProjection", "createProjection");Class IMediaProjectionManager = Class.forName("android.media.projection.IMediaProjectionManager");// public IMediaProjection createProjection(int uid, String packageName, int type, boolean isPermanentGrant)Method createProjection = IMediaProjectionManager.getDeclaredMethod("createProjection", Integer.TYPE, String.class, Integer.TYPE, Boolean.TYPE);Object projection = createProjection.invoke(mService, android.os.Process.myUid(), App.getApp().getPackageName(),0, false);Logger.i("createProjection", "projection created!");//android.media.projection.IMediaProjection;Class IMediaProjection = IInterface.class;//Class.forName("android.media.projection.IMediaProjection");Method asBinder = IMediaProjection.getDeclaredMethod("asBinder");Logger.i("createProjection", "asBinder found");Intent intent = new Intent();//    public static final String EXTRA_MEDIA_PROJECTION =//            "android.media.projection.extra.EXTRA_MEDIA_PROJECTION";//Bundle extra = new Bundle();//extra.putBinder("android.media.projection.extra.EXTRA_MEDIA_PROJECTION",  (IBinder)asBinder.invoke(projection));//intent.putExtra("android.media.projection.extra.EXTRA_MEDIA_PROJECTION",  (IBinder)asBinder.invoke(projection));intent.putExtra(Intent.EXTRA_RETURN_RESULT, Activity.RESULT_OK);Object projBinder = asBinder.invoke(projection);Logger.i("createProjection", "asBinder invoke success.");//intent.getExtras().putBinder("android.media.projection.extra.EXTRA_MEDIA_PROJECTION",  (IBinder)projBinder);Method putExtra = Intent.class.getDeclaredMethod("putExtra", String.class, IBinder.class);putExtra.invoke(intent, "android.media.projection.extra.EXTRA_MEDIA_PROJECTION",  (IBinder)projBinder);Logger.i("createProjection", "putExtra with IBinder success.");MediaProjectionManager projMgr = App.getApp().getMediaProjectionManager();MediaProjection mp = projMgr.getMediaProjection(Activity.RESULT_OK, intent);Logger.i("createProjection", "getMediaProjection " + (mp == null ? " Failed" : "Success"));//new MediaProjection(mContext, IMediaProjection.Stub.asInterface(projection));//if(mp != null)mp.stop();return  mp;} catch (ClassNotFoundException e) {e.printStackTrace();} catch (NoSuchMethodException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}return null;}

参考

Android截屏录屏MediaProjection分享
Android录屏的三种方案
媒体投影
[Android] 使用MediaProjection截屏
android设备间实现无线投屏

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

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

相关文章

SpringBoot源码解析(七):应用上下文结构体系

SpringBoot源码系列文章 SpringBoot源码解析(一)&#xff1a;SpringApplication构造方法 SpringBoot源码解析(二)&#xff1a;引导上下文DefaultBootstrapContext SpringBoot源码解析(三)&#xff1a;启动开始阶段 SpringBoot源码解析(四)&#xff1a;解析应用参数args Sp…

基于预共享密钥的IPsec实验

一、实验目的 &#xff08;1&#xff09;了解IPsec的原理和协议运行机制&#xff1b; &#xff08;2&#xff09;掌握IPsec身份认证的预共享密钥的配置&#xff1b; &#xff08;3&#xff09;掌握用Wireshark工具抓包分析IPsec数据包格式和协议流程。 二、实验设备与环境 &…

IIO(Industrial I/O)驱动介绍

文章目录 IIO&#xff08;Industrial I/O&#xff09;驱动是Linux内核中用于工业I/O设备的子系统&#xff0c;主要用于处理传感器数据采集和转换。以下是其关键点&#xff1a; 功能 数据采集&#xff1a;从传感器读取数据。数据处理&#xff1a;对原始数据进行滤波、校准等操作…

解决关于Xcode16提交审核报错

# 问题描述 The following issues occurred while distributing your application. Asset validation failed Invalid Executable. The executable xxx.app/Frameworks/HappyDNS.framework/HappyDNS contains bitcode.(lD:ef5dd249-731f-4731-8173-8e4a12519352) Asset valida…

PenGymy论文阅读

这里发现idea被人家先发了&#xff0c;没办法&#xff0c;资料收集的不够全面&#xff0c;现在来学习一下这个项目 这篇论文的贡献如下&#xff1a; 总的来说&#xff0c;他的主要工作是构建逼真的仿真环境&#xff0c;然后根据这个仿真环境生成真实的靶场&#xff0c;使得这个…

JavaWeb 前端基础 html + CSS 快速入门 | 018

今日推荐语 指望别人的救赎&#xff0c;势必走向毁灭——波伏娃 日期 学习内容 打卡编号2025年01月17日JavaWeb 前端基础 html CSS018 前言 哈喽&#xff0c;我是菜鸟阿康。 今天 正式进入JavaWeb 的学习&#xff0c;简单学习 html CSS 这2各前端基础部分&am…

从零搭建SpringBoot3+Vue3前后端分离项目基座,中小项目可用

文章目录 1. 后端项目搭建 1.1 环境准备1.2 数据表准备1.3 SpringBoot3项目创建1.4 MySql环境整合&#xff0c;使用druid连接池1.5 整合mybatis-plus 1.5.1 引入mybatis-plus1.5.2 配置代码生成器1.5.3 配置分页插件 1.6 整合swagger3&#xff08;knife4j&#xff09; 1.6.1 整…

大文件上传服务-后端V1V2

文章目录 大文件上传概述:minio分布式文件存储使用的一些技术校验MD5的逻辑 uploadV1 版本 1uploadv2 版本 2 大文件上传概述: 之前项目做了一个文件上传的功能,最近看到有面试会具体的问这个上传功能的细节&#xff0c;把之前做的项目拿过来总结一下&#xff0c;自己写的一个…

【机器学习】鲁棒(健壮)回归-Theil-Sen估计(Theil-Sen Estimator)

Theil-Sen估计 Theil-Sen估计是一种用于线性回归的非参数方法&#xff0c;其优点是对离群点具有鲁棒性。它通过计算数据点之间所有可能斜率的中位数来估计回归线的斜率&#xff0c;随后使用这些斜率估算截距。 核心思想 斜率估计&#xff1a; 对于给定的一组数据点 &#xff0…

配置Kubernetes从节点与集群Calico网络

在上一篇博客中&#xff0c;我们成功安装并初始化了Kubernetes的主节点&#xff0c;并且看到了集群初始化成功的标志信息。接下来&#xff0c;我们将继续安装从节点&#xff08;worker nodes&#xff09;&#xff0c;以构建一个完整的Kubernetes集群。 步骤回顾 在上一步中&a…

【数据库】MySQL数据库SQL语句汇总

目录 1.SQL 通用语法 2.SQL 分类 2.1.DDL 2.2.DML 2.3.DQL 2.4.DCL 3.DDL 3.1.数据库操作 3.1.1.查询 3.1.2.创建 3.1.3.删除 3.1.4.使用 3.2.表操作 3.2.1.查询 3.2.2.创建 3.2.3.数据类型 3.2.3.1.数值类型 3.2.3.2.字符串类型 3.2.3.3.日期时间类型 3.2…

做跨境电商服务器用什么宽带好?

做跨境电商服务器用什么宽带好&#xff1f;做跨境电商服务器&#xff0c;推荐选择光纤宽带或高性能的5G网络。光纤宽带高速稳定&#xff0c;适合处理大量数据和实时交互&#xff1b;5G网络则提供超高速移动连接&#xff0c;适合需要灵活性和移动性的卖家。具体选择需根据业务规…

光谱相机的光谱分辨率可以达到多少?

多光谱相机 多光谱相机的光谱分辨率相对较低&#xff0c;波段数一般在 10 到 20 个左右&#xff0c;光谱分辨率通常在几十纳米到几百纳米之间&#xff0c;如常见的多光谱相机光谱分辨率为 100nm 左右。 高光谱相机 一般的高光谱相机光谱分辨率可达 2.5nm 到 10nm 左右&#x…

Python毕业设计选题:基于django+vue的智能租房系统的设计与实现

开发语言&#xff1a;Python框架&#xff1a;djangoPython版本&#xff1a;python3.7.7数据库&#xff1a;mysql 5.7数据库工具&#xff1a;Navicat11开发软件&#xff1a;PyCharm 系统展示 租客注册 添加租客界面 租客管理 房屋类型管理 房屋信息管理 系统管理 摘要 本文首…

[Qualcomm]Qualcomm MDM9607 SDK代码下载操作说明

登录Qualcomm CreatePoing Qualcomm CreatePointhttps://createpoint.qti.qua

PE文件:节表-添加节

在所有节的空白区域都不够存放我们想要添加的数据时&#xff0c;这个时候可以通过添加节来扩展我们可操作的空间去存储新的数据&#xff08;如导入表、代码或资源&#xff09;。 过程步骤 1.判断是否有足够的空间添加节表 PE文件的节表紧跟在PE头之后&#xff0c;每个节表的…

图论的起点——七桥问题

普瑞格尔河从古堡哥尼斯堡市中心流过&#xff0c;河中有小岛两座&#xff0c;筑有7座古桥&#xff0c;哥尼斯堡人杰地灵&#xff0c;市民普遍爱好数学。1736年&#xff0c;该市一名市民向大数学家Euler提出如下的所谓“七桥问题”&#xff1a; 从家里出发&#xff0c;7座桥每桥…

Fabric区块链网络搭建:保姆级图文详解

目录 前言1、项目环境部署1.1 基础开发环境1.2 网络部署 2、后台环境2.1、环境配置2.2、运行springboot项目 3、PC端3.1、安装依赖3.2、修改区块链网络连接地址3.3、启动项目 前言 亲爱的家人们&#xff0c;创作很不容易&#xff0c;若对您有帮助的话&#xff0c;请点赞收藏加…

02JavaWeb——JavaScript-Vue(项目实战)

一、JavaScript html完成了架子&#xff0c;css做了美化&#xff0c;但是网页是死的&#xff0c;我们需要给他注入灵魂&#xff0c;所以接下来我们需要学习 JavaScript&#xff0c;这门语言会让我们的页面能够和用户进行交互。 1.1 介绍 通过JS/js效果演示提供资料进行效果演…

Windows 蓝牙驱动开发-蓝牙设备栈

蓝牙设备栈 蓝牙驱动程序堆栈包含 Microsoft 为蓝牙协议提供支持的核心部分。 有了这个堆栈&#xff0c;已启用蓝牙的设备可以彼此定位并建立连接。 在此类连接中&#xff0c;设备可以通过各种应用程序交换数据并彼此交互。 下图显示了蓝牙驱动程序堆栈中的模块&#xff0c;以…