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.rhkb.cn/news/2006.html

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

相关文章

PostgreSQL技术内幕22:vacuum full 和 vacuum

文章目录 0.简介1.概念及使用方式2.工作原理2.1 主要功能2.2 清理流程2.3 防止事务id环绕说明 3.使用建议 0.简介 在之前介绍MVCC文章中介绍过常见的MVCC实现的两种方式&#xff0c;一种是将旧数据放到回滚段&#xff0c;一种是直接生成一条新数据&#xff08;对于删除是不删除…

C#,图片分层(Layer Bitmap)绘制,反色、高斯模糊及凹凸贴图等处理的高速算法与源程序

1 图像反色Invert 对图像处理的过程中会遇到一些场景需要将图片反色&#xff0c;反色就是取像素的互补色&#xff0c;比如当前像素是0X00FFFF&#xff0c;对其取反色就是0XFFFFFF – 0X00FFFF 0XFF0000&#xff0c;依次对图像中的每个像素这样做&#xff0c;最后得到的就是原…

STM32 FreeRTOS 的任务挂起与恢复以及查看任务状态

目录 任务的挂起与恢复的API函数 任务挂起函数 任务恢复函数 任务恢复函数&#xff08;中断中恢复&#xff09; 函数说明 注意事项 查看任务状态 任务的挂起与恢复的API函数 vTaskSuspend()&#xff1a;挂起任务, 类似暂停&#xff0c;可恢复 vTaskResume()&#xff1a…

vscode 扩展Cline、Continue的差别?

Cline和Continue都是VSCode的AI编程插件&#xff0c;它们在功能、用户体验、性能、适用场景以及配置和使用步骤等方面存在一些差别&#xff1a; 一、功能差异 编辑功能 Cline&#xff1a;能够分析项目的文件结构和源代码抽象语法树&#xff08;AST&#xff09;&#xff0c;通…

Unity 3D游戏开发从入门进阶到高级

本文精心整理了Unity3D游戏开发相关的学习资料&#xff0c;涵盖入门、进阶、性能优化、面试和书籍等多个维度&#xff0c;旨在为Unity开发者提供全方位、高含金量的学习指南.欢迎收藏。 学习社区 Unity3D开发者 这是一个专注于Unity引擎的开发者社区&#xff0c;汇聚了众多Un…

LLM实现视频切片合成 前沿知识调研

1.相关产品 产品链接腾讯智影https://zenvideo.qq.com/可灵https://klingai.kuaishou.com/即梦https://jimeng.jianying.com/ai-tool/home/Runwayhttps://aitools.dedao.cn/ai/runwayml-com/Descripthttps://www.descript.com/?utm_sourceai-bot.cn/Opus Cliphttps://www.opu…

1Hive概览

1Hive概览 1hive简介2hive架构3hive与Hadoop的关系4hive与传统数据库对比5hive的数据存储 1hive简介 Hive是基于Hadoop的一个数据仓库工具&#xff0c;可以将结构化的数据文件映射为一张数据库表&#xff0c;并提供类SQL查询功能。 其本质是将SQL转换为MapReduce/Spark的任务进…

IDEA的Java注释在Toggle Rendered View下的字号调整方式

记录IntelliJ IDEA的Java注释在Toggle Rendered View下的字号调整方式 如图&#xff0c;在Toggle Rendered View模式下的注释字号很大&#xff0c;与代码不协调&#xff0c;在此区域点击鼠标右键&#xff0c;选中 Adjust 出现一个滑动条&#xff0c;通过拖动游标调整字号大小…

游戏市场成果及趋势

2024 年的游戏行业发展情况如何&#xff1f;这是一个既关系到开发商&#xff0c;又关系到玩家的问题&#xff0c;而市场分析师可以为我们揭晓答案。下面&#xff0c;就让我们来看看分析师给出的结论以及他们对未来趋势的预测。 玩家 自 2021 年起&#xff0c;全球平均游戏时间…

C++复习

注&#xff1a;本文章所写内容是小编复习所看的。记录的是一些之前模糊不清的知识点。详细c内容请移步至小编主页寻找。 竞赛小技巧 竞赛中cin/cout用不了&#xff08;没有办法刷新缓冲区&#xff0c;导致cin/cout与缓冲区绑定&#xff09; 解决办法&#xff1a;(加以下三行…

【C++】多线程

目录 多线程基础什么是线程线程和进程的关系线程的特点什么是多线程编程为什么要使用多线程线程与CPU的执行关系线程的生命周期 创建线程&#xff08;C11&#xff09;线程的可调用对象传参数 注意事项join和detach的区别一个线程包含什么东西this_thread 线程同步线程同步机制互…

《深度剖析算法优化:提升效率与精度的秘诀》

想象一下&#xff0c;你面前有一堆杂乱无章的数据&#xff0c;你需要从中找到特定的信息&#xff0c;或者按照一定的规则对这些数据进行排序。又或者&#xff0c;你要为一个物流公司规划最佳的配送路线&#xff0c;以降低成本和提高效率。这些问题看似复杂&#xff0c;但都可以…

怎么实现Redis的高可用?

大家好&#xff0c;我是锋哥。今天分享关于【怎么实现Redis的高可用&#xff1f;】面试题。希望对大家有帮助&#xff1b; 怎么实现Redis的高可用&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 为了实现 Redis 的高可用性&#xff0c;我们需要保证在发…

【CSS】HTML页面定位CSS - position 属性 relative 、absolute、fixed 、sticky

目录 relative 相对定位 absolute 绝对定位 fixed 固定定位 sticky 粘性定位 position&#xff1a;relative 、absolute、fixed 、sticky &#xff08;四选一&#xff09; top&#xff1a;距离上面的像素 bottom&#xff1a;距离底部的像素 left&#xff1a;距离左边的像素…

使用docker-compose安装Redis的主从+哨兵模式

必看 本文是一主二从一哨兵模式&#xff1b;其余的单机/集群/多哨兵模式的话&#xff0c;不在本文... 本文的环境主要是&#xff1a;应用app在本地&#xff0c;redis在云服务器上&#xff1b; 图解 图如下&#xff1a;这个图很重要&#xff1b; 之所以要这样画图&#xff0…

深度剖析RabbitMQ:从基础组件到管理页面详解

文章目录 一、简介二、Overview2.1 Overview->Totals2.2 Overview->Nodesbroker的属性2.3 Overview->Churn statistics2.4 Overview->Ports and contexts2.5 Overview->Export definitions2.6 Overview->Import definitions 三、Connections连接的属性 四、C…

[0405].第05节:搭建Redis主从架构

Redis学习大纲 一、3主3从的集群配置&#xff1a; 1.1.集群规划 1.分片集群需要的节点数量较多&#xff0c;这里我们搭建一个最小的分片集群&#xff0c;包含3个master节点&#xff0c;每个master包含一个slave节点&#xff0c;结构如下&#xff1a; 2.每组是一主一从&#x…

QT在 MacOS X上,如何检测点击程序坞中的Dock图标

最近在开发MacOS的qt应用&#xff0c;在做到最小化系统托盘功能时&#xff0c;发现关闭窗口后再次点击程序坞中的Dock图标不能将主界面再显示出来。查询里很多资料&#xff0c;发现是QT自身的问题&#xff0c;没有做相关的点击Dock图标的处理。 于是我参考了国内和国外的这两篇…

Flutter插件制作、本地/远程依赖及缓存机制深入剖析(原创-附源码)

Flutter插件在开发Flutter项目的过程中扮演着重要的角色&#xff0c;我们从 ​​​​​​https://pub.dev 上下载添加到项目中的第三方库都是以包或者插件的形式引入到代码中的&#xff0c;这些第三方工具极大的提高了开发效率。 深入的了解插件的制作、发布、工作原理和缓存机…

每日学习30分轻松掌握CursorAI:Cursor插件系统与扩展功能

Cursor插件系统与扩展功能 一、课程概述 今天我们将学习Cursor AI的插件系统&#xff0c;了解如何通过插件扩展和增强IDE功能。由于Cursor AI基于VS Code开发&#xff0c;我们可以利用丰富的VS Code插件生态系统。 1.1 学习目标 了解插件系统原理掌握插件安装管理使用常用开…