从车窗升降一探 Android 车机的重要 API:车辆属性 CarProperty
前言
前面我们介绍过 Android 车机 Automotive OS 的几块重要内容:
- 一文了解 Android 车机如何处理中控的旋钮输入
- 从实体按键看 Android 车机的自定义事件机制
- 深度入门 Android 车机核心 CarService 的构成和链路
本篇文章我们聚焦 Android 车机上最重要、最常用的接口:即车辆属性 CarPropertyManager
。
并结合车窗升降这种典型的场景来探究它的完整链路。
实现车窗升降
CarPropertyManager
通常针对某个 Property
发起读写,这些属性有很多,从车窗到空调、油量到续航等等。
想要控制它们,得需要知道它的唯一标识,并和系统定义的 ID 保持一致。那么车窗对应的 ID 为 VehiclePropertyIds
中的 WINDOW_POS
,其要求 app 拥有专用的权限:
android.car.Car.PERMISSION_CONTROL_CAR_WINDOWS
属性监听
当目标属性发生变化,可以通过 CarPropertyEventCallback
通知到请求 App,为了满足各种场景,系统提供了设置通知频次的可能。
总共有如下几种:
通知频次类型 | 频次(HZ) |
---|---|
SENSOR_RATE_ONCHANGE | - |
SENSOR_RATE_FASTEST | 100 |
SENSOR_RATE_FAST | 10 |
SENSOR_RATE_NORMAL | 1 |
SENSOR_RATE_UI | 5 |
对于车窗、入座这些即时信号,采用 SENSOR_RATE_ONCHANGE 类型即可,意味着只在变化的时候通知。当然,注册的时候会立即回调一次以通知当前的数值。
代码很简单,构建 CarPropertyEventCallback 实例,并传递目标 Property ID 和上述的通知类型,即可完成该属性的监听。
class CarEventCallBack: CarPropertyManager.CarPropertyEventCallback {override fun onChangeEvent(value: CarPropertyValue<*>?) { }
}val car = Car.createCar(context)
val carPropertyManager =car?.getCarManager(Car.PROPERTY_SERVICE) as CarPropertyManagercarPropertyManager.registerCallback(CarEventCallBack(),VehiclePropertyIds.WINDOW_POS,CarPropertyManager.SENSOR_RATE_ONCHANGE
)
属性读写
对于车窗硬件来说,用户关心的是其升降的状况,系统用 0~100 来进行定义,继而决定了它的值为 Int 型。
那么读取的 API 为 getIntProperty()
,参数:
- prop:希望读取的属性 ID,比如上面的车窗 Property ID:WINDOW_POS
- area:希望读取属性的位置信息 zone,对应到
VehicleAreaWindow
类型中常量
注意:该方法是同步的,而且因为车窗等属性的操作耗时,建议在子线程 invoke。
写入的 API 为 setIntProperty()
,参数:
- prop:希望改写的属性 ID,
- areaId:该属性对应的位置薪资
- val:Value to set,比如车窗即 0 ~ 100,对应着完全打开到完全关闭
和 getIntProperty() 一样,set 一样耗时,需要同样运行在子线程中。
系统预设的和 Window 相关的 zone areaId 如下,比如前排、驾驶侧、副驾驶侧、乘客侧、天窗、挡风玻璃等。
package android.hardware.automotive.vehicle;public @interface VehicleAreaWindow {public static final int FRONT_WINDSHIELD = 1;public static final int REAR_WINDSHIELD = 2;public static final int ROW_1_LEFT = 16;public static final int ROW_1_RIGHT = 64;public static final int ROW_2_LEFT = 256;public static final int ROW_2_RIGHT = 1024;public static final int ROW_3_LEFT = 4096;public static final int ROW_3_RIGHT = 16384;public static final int ROOF_TOP_1 = 65536;public static final int ROOF_TOP_2 = 131072;
}
如下代码展示如何了完全打开驾驶位车窗。
Thread().run {carPropertyManager.setIntProperty(VehiclePropertyIds.WINDOW_POS,VehicleAreaWindow.WINDOW_ROW_1_LEFT,0)
}
工作原理
首先,车窗相关的 area 在 HAL 层有相应的定义:
// android/hardware/automotive/vehicle/2.0/types.h /*** Various windshields/windows in the car.*/
enum class VehicleAreaWindow : int32_t {FRONT_WINDSHIELD = 1 /* 0x00000001 */,REAR_WINDSHIELD = 2 /* 0x00000002 */,ROW_1_LEFT = 16 /* 0x00000010 */,ROW_1_RIGHT = 64 /* 0x00000040 */,ROW_2_LEFT = 256 /* 0x00000100 */,ROW_2_RIGHT = 1024 /* 0x00000400 */,ROW_3_LEFT = 4096 /* 0x00001000 */,ROW_3_RIGHT = 16384 /* 0x00004000 */,ROOF_TOP_1 = 65536 /* 0x00010000 */,ROOF_TOP_2 = 131072 /* 0x00020000 */,
};
读取
直接看 getIntProperty()
,首先调用 checkSupportedProperty() 检查是否支持该属性,当不支持的话抛出:
IllegalArgumentException: “Unsupported property:xxx”
接着调用 getProperty()
,不过指定了返回的数据类型。
public class CarPropertyManager extends CarManagerBase {public int getIntProperty(int prop, int area) {checkSupportedProperty(prop);CarPropertyValue<Integer> carProp = getProperty(Integer.class, prop, area);return handleNullAndPropertyStatus(carProp, area, 0);}private void checkSupportedProperty(int propId) {switch (propId) {case VehiclePropertyIds.INITIAL_USER_INFO:case VehiclePropertyIds.SWITCH_USER:case VehiclePropertyIds.CREATE_USER:case VehiclePropertyIds.REMOVE_USER:case VehiclePropertyIds.USER_IDENTIFICATION_ASSOCIATION:throw new IllegalArgumentException("Unsupported property: "+ VehiclePropertyIds.toString(propId) + " (" + propId + ")");}}...
}
getProperty() 的实现在于 CarPropertyService
。
public class CarPropertyManager extends CarManagerBase {public <E> CarPropertyValue<E> getProperty(@NonNull Class<E> clazz, int propId, int areaId) {checkSupportedProperty(propId);try {CarPropertyValue<E> propVal = mService.getProperty(propId, areaId);if (propVal != null && propVal.getValue() != null) {Class<?> actualClass = propVal.getValue().getClass();}return propVal;}...}...
}
CarPropertyService
按照如下步骤进行:
-
先到存放所有 Property ID 的
SparseArray
中检查是否确实存在该 Property,如果不存在的话打印 error 提醒并结束 -
获取该 Property 的 permission 配置,如果不存在的话,抛出:
SecurityException: Platform does not have permission to read value for property Id: 0x…
-
assertPermission()
检查当前 CarService 是否确实被授予了如上 permission -
最后调用持有的
PropertyHalService
继续发出读取的调用
public class CarPropertyService extends ICarProperty.Stubimplements CarServiceBase, PropertyHalService.PropertyHalListener {@Overridepublic CarPropertyValue getProperty(int prop, int zone) ... {synchronized (mLock) {if (mConfigs.get(prop) == null) {// Do not attempt to register an invalid propIdSlogf.e(TAG, "getProperty: propId is not in config list:0x" + toHexString(prop));return null;}}// Checks if android has permission to read property.String permission = mHal.getReadPermission(prop);if (permission == null) {throw new SecurityException("Platform does not have permission to read value for "+ "property Id: 0x" + Integer.toHexString(prop));}CarServiceUtils.assertPermission(mContext, permission);return runSyncOperationCheckLimit(() -> {return mHal.getProperty(prop, zone);});}...
}
PropertyHalService 首先调用 managerToHalPropId() 将 Property ID 转为 HAL 中该 ID 的定义,并再度检查该 HAL ID 是否确实存在。如果不存在的话亦抛出:
IllegalArgumentException:Invalid property Id : 0x…
接着,通过 VehicleHal
传递 HAL 中 ID 继续读取得到 HalPropValue
,当读取的 value 存在的话,首先得获取该 Property 在 HAL 层和上层定义的 HalPropConfig
规则。
最后依据 config 将 value 解析成 CarPropertyValue
类型返回。
public class PropertyHalService extends HalServiceBase {
'/ ' ...public CarPropertyValue getProperty(int mgrPropId, int areaId)throws IllegalArgumentException, ServiceSpecificException {int halPropId = managerToHalPropId(mgrPropId);if (!isPropertySupportedInVehicle(halPropId)) {throw new IllegalArgumentException("Invalid property Id : 0x" + toHexString(mgrPropId));}// CarPropertyManager catches and rethrows exception, no need to handle here.HalPropValue value = mVehicleHal.get(halPropId, areaId);if (value == null) {return null;}HalPropConfig propConfig;synchronized (mLock) {propConfig = mHalPropIdToPropConfig.get(halPropId);}return value.toCarPropertyValue(mgrPropId, propConfig);}...
}
其实 VehicleHal 并未做太多处理就直接交给了 HalClient 来处理。
public class VehicleHal implements HalClientCallback {...public HalPropValue get(int propertyId)throws IllegalArgumentException, ServiceSpecificException {return get(propertyId, NO_AREA);}...public HalPropValue get(int propertyId, int areaId)throws IllegalArgumentException, ServiceSpecificException {return mHalClient.getValue(mPropValueBuilder.build(propertyId, areaId));}...
}
HalClient
通过 invokeRetriable()
进行超时为 50ms 的 internalGet()
调用:如果结果是 TRY_AGAIN 并且尚未超时的话,再次调用;反之已经超时或者结果成功获取到的话,即结束。
后续会再次检查该 Result 中的 status,是否是不合法的、空的值等等,通过检查的话则返回 HalPropValue
出去。
final class HalClient {...private static final int SLEEP_BETWEEN_RETRIABLE_INVOKES_MS = 50;HalPropValue getValue(HalPropValue requestedPropValue)throws IllegalArgumentException, ServiceSpecificException {ObjectWrapper<ValueResult> resultWrapper = new ObjectWrapper<>();resultWrapper.object = new ValueResult();int status = invokeRetriable(() -> {resultWrapper.object = internalGet(requestedPropValue);return resultWrapper.object.status;}, mWaitCapMs, mSleepMs);ValueResult result = resultWrapper.object;if (StatusCode.INVALID_ARG == status) {throw new IllegalArgumentException(getValueErrorMessage("get", requestedPropValue, result.errorMsg));}if (StatusCode.OK != status || result.propValue == null) {if (StatusCode.OK == status) {status = StatusCode.NOT_AVAILABLE;}throw new ServiceSpecificException(status, getValueErrorMessage("get", requestedPropValue, result.errorMsg));}return result.propValue;}private ValueResult internalGet(HalPropValue requestedPropValue) {final ValueResult result = new ValueResult();try {result.propValue = mVehicle.get(requestedPropValue);result.status = StatusCode.OK;result.errorMsg = new String();}...return result;}...
}
internalGet() 的实现由持有的 VehicleStub 实例的 get 方法完成,其实现对应于依据 HIDL 的配置调用 HAL 侧获取相应数据。
public abstract class VehicleStub {...@Nullablepublic abstract HalPropValue get(HalPropValue requestedPropValue)throws RemoteException, ServiceSpecificException;...
}
写入
set 写入的链路和 get 大同小异,主要区别是:
- 事先构建待写入的属性实例
CarPropertyValue
并传入 - 传入属性变化时 callback 用的
CarPropertyEventListenerToService
实例
public class CarPropertyManager extends CarManagerBase {public void setIntProperty(int prop, int areaId, int val) {setProperty(Integer.class, prop, areaId, val);}public <E> void setProperty(@NonNull Class<E> clazz, int propId, int areaId, @NonNull E val) {checkSupportedProperty(propId);try {runSyncOperation(() -> {mService.setProperty(new CarPropertyValue<>(propId, areaId, val),mCarPropertyEventToService);return null;});}...}
}
下一层 CarPropertyService
的实现也是通过 PropertyHalService
进行。
传入的 CarPropertyEventListenerToService 其实是 ICarPropertyEventListener AIDL 代理,这里会将其转为 Binder 对象,按照调用的源头 client 缓存起来,在属性变化的时候用。
public class CarPropertyService extends ICarProperty.Stubimplements CarServiceBase, PropertyHalService.PropertyHalListener {public void setProperty(CarPropertyValue prop, ICarPropertyEventListener listener)throws IllegalArgumentException, ServiceSpecificException {int propId = prop.getPropertyId();...runSyncOperationCheckLimit(() -> {mHal.setProperty(prop);return null;});IBinder listenerBinder = listener.asBinder();synchronized (mLock) {Client client = mClientMap.get(listenerBinder);if (client == null) {client = new Client(listener);}if (client.isDead()) {Slogf.w(TAG, "the ICarPropertyEventListener is already dead");return;}mClientMap.put(listenerBinder, client);updateSetOperationRecorderLocked(propId, prop.getAreaId(), client);}}...
}
继续分发到 VehicleHal
侧。
public class PropertyHalService extends HalServiceBase {public void setProperty(CarPropertyValue prop)throws IllegalArgumentException, ServiceSpecificException {int halPropId = managerToHalPropId(prop.getPropertyId());...HalPropValue halPropValue = mPropValueBuilder.build(prop, halPropId, propConfig);// CarPropertyManager catches and rethrows exception, no need to handle here.mVehicleHal.set(halPropValue);}...
}
后续一样的是通过 VehicleHal
到 HalClient
,再到 VehicleStub
,最后抵达 HAL。
public class VehicleHal implements HalClientCallback {...public void set(HalPropValue propValue)throws IllegalArgumentException, ServiceSpecificException {mHalClient.setValue(propValue);}
}final class HalClient {...public void setValue(HalPropValue propValue)throws IllegalArgumentException, ServiceSpecificException {ObjectWrapper<String> errorMsgWrapper = new ObjectWrapper<>();errorMsgWrapper.object = new String();int status = invokeRetriable(() -> {try {mVehicle.set(propValue);errorMsgWrapper.object = new String();return StatusCode.OK;}...}, mWaitCapMs, mSleepMs);...}...
}public abstract class VehicleStub {...public abstract void set(HalPropValue propValue)throws RemoteException, ServiceSpecificException;...
}
结语
结合一张图回顾下整个过程:
- App 先通过 Car lib 拿到
CarService
的Car
实例,CarService 会初始化所有 Car 相关的实现,比如其中车辆属性的化,会初始化CarPropertyService
和PropertyHalService
等 - 接着,App 会从 Car 实例获取车辆某个接口的实例,比如控制车辆属性的话,需要获取
CarPropertyManager
,CarService 则会从初始化完成的 map 里返回已准备好的对应对象 - App 的属性读写会通过 AIDL 接口抵达直接负责的 CarPropertyService,然后到与 HAL 中车辆属性模块交互的
PropertyHalService
,再到综合的VehicleHal
,最后通过 HIDL 接口抵达以及更下面的Hal
,并按照定义的数据类型更改 ECU 的相关属性
希望本文能言简意赅地带你了解车辆属性的大体全貌,感谢阅读。
推荐阅读
- 一文了解 Android 车机如何处理中控的旋钮输入
- 从实体按键看 Android 车机的自定义事件机制
- 深度入门 Android 车机核心 CarService 的构成和链路
- Android 车机初体验:Auto,Automotive 傻傻分不清楚?
参考资料
- CarPropertyManager
相关文章:

从车窗升降一探 Android 车机的重要 API:车辆属性 CarProperty
前言 前面我们介绍过 Android 车机 Automotive OS 的几块重要内容: 一文了解 Android 车机如何处理中控的旋钮输入从实体按键看 Android 车机的自定义事件机制深度入门 Android 车机核心 CarService 的构成和链路 本篇文章我们聚焦 Android 车机上最重要、最常用…...

Unity读取写入Excel
1.在Plugins中放入dll,118开头的dll在Unity安装目录下(C:\Program Files\Unity\Editor\Data\Mono\lib\mono\unity) 2.写Excel public void WriteExcel(){//文件地址FileInfo newFile new FileInfo(Application.dataPath "/test.xlsx…...

手搭手Ajax经典基础案例省市联动
环境介绍 技术栈 springbootmybatis-plusmysql 软件 版本 mysql 8 IDEA IntelliJ IDEA 2022.2.1 JDK 1.8 Spring Boot 2.7.13 mybatis-plus 3.5.3.2 pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http:/…...

分类预测 | MATLAB实现SSA-CNN-BiLSTM-Attention数据分类预测(SE注意力机制)
分类预测 | MATLAB实现SSA-CNN-BiLSTM-Attention数据分类预测(SE注意力机制) 目录 分类预测 | MATLAB实现SSA-CNN-BiLSTM-Attention数据分类预测(SE注意力机制)分类效果基本描述模型描述程序设计参考资料 分类效果 基本描述 1.MAT…...
Springboot后端开发_日志
SpringBoot_日志 简介1、日志框架2、SLF4j使用1、如何在系统中使用SLF4j https://www.slf4j.org2、遗留问题 3、SpringBoot日志关系4、日志使用1、默认配置2、指定配置 5、切换日志框架拓展:日志分组 简介 6 种日志级别 TRACE: designates finer-grained informat…...
Unable to connect to the server: x509: certificate is valid for问题解决
文章目录 环境描述问题描述问题原因解决方案额外问题问题描述问题解决方案新问题 环境描述 Kubernetes版本1.15测试客户端centos7 问题描述 将构建于内网网络环境上的kubernetes集群的/etc/kubernetes/admin.conf文件拷贝到外网的一台装有kubernetes客户端的设备上ÿ…...

使用vite搭建前端项目
1、在vscode 终端那里执行创建前端工程项目,其中shop-admin为项目名称: npm init vite-app shop-admin 提示如需安装其他依赖执行npm install ....,否则忽略(第三步再讲)。 2、执行npm run dev 命令直接运行创建好的项目,在浏览器打开链接…...
leetcode1658. 将 x 减到 0 的最小操作数
题目链接:1658. 将 x 减到 0 的最小操作数 - 力扣(LeetCode) 知道滑动窗口,代码却写不出来 #define MIN(a ,b) ((a) < (b) ? (a) : (b))int minOperations(int* nums, int numsSize, int x) {int ans INT_MAX;int sum 0;f…...

Jenkins 重新定义 pom 内容,打包
文章目录 源码管理构建 源码管理 添加仓库地址,拉取凭证,选择需要的分支 构建 勾选 构建环境 下删除原始 build 配置,防止文件错误 Pre Steps 构建前处理 pom.xml ,例如我是需要删除该模块的所有子模块配置,我这里…...

自然语言处理---Transformer构建语言模型
语言模型概述 以一个符合语言规律的序列为输入,模型将利用序列间关系等特征,输出一个在所有词汇上的概率分布,这样的模型称为语言模型。 # 语言模型的训练语料一般来自于文章,对应的源文本和目标文本形如: src1 "I can do&…...
【WPF】对Image元素进行缩放平移等操作
元素布局 <Border Grid.Row"1" Name"border" ClipToBounds"True" Margin"10,10,10,10"><Image Name"image" Visibility"Visible" Margin"3,3,3,3" Grid.Column"1" Source"{Bin…...
JavaScript中Bom节点和表单的获取值
Bom节点 代表浏览器对象模型(Browser Object Model),它是浏览器提供的 JavaScript API,用于与浏览器窗口和浏览器本身进行交互 获取当前网页的URL: const currentURL window.location.href; console.log(currentURL…...

RDB.js:适用于 Node.js 和 Typescript 的终极对象关系映射器
RDB.js 是适用于 Node.js 和 Typescript 的终极对象关系映射器,可与 Postgres、MS SQL、MySQL、Sybase SAP 和 SQLite 等流行数据库无缝集成。无论您是使用 TypeScript 还是 JavaScript(包括 CommonJS 和 ECMAScript)构建应用程序,…...

ROI的投入产出比是什么?
ROI的投入产出比是什么? 投入产出比(Return on Investment, ROI)是一种评估投资效益的财务指标,用于衡量投资带来的回报与投入成本之间的关系。它的计算公式如下: 投资收益:指的是投资带来的净收入&#x…...
Linux打包发布常用命令
1、先下载一个FileZilla Client远程连接工具,并连接我们需要连接的服务器 2、进入xshell连接对应的服务器,连接后若不知道项目位置,可使用此命令查看 ps -ef | grep java 此时会出现一大串代码,找到以我这为例:root…...

Docker Swarm 节点维护
Docker Swarm Mode Docker Swarm 集群搭建 Docker Swarm 节点维护 Docker Service 创建 1.角色转换 Swarm 集群中节点的角色只有 manager 与 worker,所以其角色也只是在 manager 与worker 间的转换。即 worker 升级为 manager,或 manager 降级为 worke…...
AS/NZS 1859.3:2017 木基装饰板检测
木基装饰板是指以木质材料为基材,比如刨花板,胶合板等木质人造板,表面贴有PVC膜,三聚氰胺纸,木饰面等装饰层压制而成的木质复合材料,主要用于墙面装饰,家具等领域。 AS/NZS 1859.3:…...

深入理解算法:从基础到实践
深入理解算法:从基础到实践 1. 算法的定义2. 算法的特性3. 算法的分类按解决问题的性质分类:按算法的设计思路分类: 4. 算法分析5. 算法示例a. 搜索算法示例:二分搜索b. 排序算法示例:快速排序c. 动态规划示例…...
华为OD 机智的外卖员(100分)【java】A卷+B卷
华为OD统一考试A卷+B卷 新题库说明 你收到的链接上面会标注A卷还是B卷。目前大部分收到的都是B卷。 B卷对应20022部分考题以及新出的题目,A卷对应的是新出的题目。 我将持续更新最新题目 获取更多免费题目可前往夸克网盘下载,请点击以下链接进入: 我用夸克网盘分享了「华为O…...

Node编写用户登录接口
目录 前言 服务器 编写登录接口API 使用sql语句查询数据库中是否有该用户 判断密码是否正确 生成JWT的Token字符串 配置解析token的中间件 配置捕获错误中间件 完整的登录接口代码 前言 本文介绍如何使用node编写登录接口以及解密生成token,如何编写注册接…...
<6>-MySQL表的增删查改
目录 一,create(创建表) 二,retrieve(查询表) 1,select列 2,where条件 三,update(更新表) 四,delete(删除表…...

51c自动驾驶~合集58
我自己的原文哦~ https://blog.51cto.com/whaosoft/13967107 #CCA-Attention 全局池化局部保留,CCA-Attention为LLM长文本建模带来突破性进展 琶洲实验室、华南理工大学联合推出关键上下文感知注意力机制(CCA-Attention),…...

智慧工地云平台源码,基于微服务架构+Java+Spring Cloud +UniApp +MySql
智慧工地管理云平台系统,智慧工地全套源码,java版智慧工地源码,支持PC端、大屏端、移动端。 智慧工地聚焦建筑行业的市场需求,提供“平台网络终端”的整体解决方案,提供劳务管理、视频管理、智能监测、绿色施工、安全管…...

《从零掌握MIPI CSI-2: 协议精解与FPGA摄像头开发实战》-- CSI-2 协议详细解析 (一)
CSI-2 协议详细解析 (一) 1. CSI-2层定义(CSI-2 Layer Definitions) 分层结构 :CSI-2协议分为6层: 物理层(PHY Layer) : 定义电气特性、时钟机制和传输介质(导线&#…...

自然语言处理——循环神经网络
自然语言处理——循环神经网络 循环神经网络应用到基于机器学习的自然语言处理任务序列到类别同步的序列到序列模式异步的序列到序列模式 参数学习和长程依赖问题基于门控的循环神经网络门控循环单元(GRU)长短期记忆神经网络(LSTM)…...
大数据学习(132)-HIve数据分析
🍋🍋大数据学习🍋🍋 🔥系列专栏: 👑哲学语录: 用力所能及,改变世界。 💖如果觉得博主的文章还不错的话,请点赞👍收藏⭐️留言Ǵ…...
鸿蒙DevEco Studio HarmonyOS 5跑酷小游戏实现指南
1. 项目概述 本跑酷小游戏基于鸿蒙HarmonyOS 5开发,使用DevEco Studio作为开发工具,采用Java语言实现,包含角色控制、障碍物生成和分数计算系统。 2. 项目结构 /src/main/java/com/example/runner/├── MainAbilitySlice.java // 主界…...

html css js网页制作成品——HTML+CSS榴莲商城网页设计(4页)附源码
目录 一、👨🎓网站题目 二、✍️网站描述 三、📚网站介绍 四、🌐网站效果 五、🪓 代码实现 🧱HTML 六、🥇 如何让学习不再盲目 七、🎁更多干货 一、👨…...

Docker 本地安装 mysql 数据库
Docker: Accelerated Container Application Development 下载对应操作系统版本的 docker ;并安装。 基础操作不再赘述。 打开 macOS 终端,开始 docker 安装mysql之旅 第一步 docker search mysql 》〉docker search mysql NAME DE…...

莫兰迪高级灰总结计划简约商务通用PPT模版
莫兰迪高级灰总结计划简约商务通用PPT模版,莫兰迪调色板清新简约工作汇报PPT模版,莫兰迪时尚风极简设计PPT模版,大学生毕业论文答辩PPT模版,莫兰迪配色总结计划简约商务通用PPT模版,莫兰迪商务汇报PPT模版,…...