# TestTuyaIpcAndroid
**Repository Path**: leesonzhong/test_tuya_ipc_android
## Basic Information
- **Project Name**: TestTuyaIpcAndroid
- **Description**: 测试涂鸦SDK,和IPC(网络摄像机)进行通信,配网和显示实时视频等。
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 2
- **Forks**: 2
- **Created**: 2021-12-07
- **Last Updated**: 2025-10-24
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 一、涂鸦 IoT 平台创建智能生活 App SDK 应用,并获取 SDK 的 AppKey,AppSecret,安全图片等信息
- https://developer.tuya.com/cn/docs/app-development/preparation?id=Ka69nt983bhh5
- 已经 [注册了涂鸦开发者账号](https://auth.tuya.com/register?from=https%3A%2F%2Fdeveloper.tuya.com%2Fcn%2Fdocs%2Fapp-development%2F),并完成实名认证
- 登录到 [涂鸦 IoT 平台](https://iot.tuya.com/oem/sdkList) 的 **App SDK** 页面
- 在 **SDK 开发** 处,单击 **创建 App**。
- 根据页面提示,选择 **App SDK类型**,选择 **智能家居**。
- 在 **填写信息** 页面,填写 SDK 对应的应用名称、包名、渠道标识符。
- 应用名称:填写您的 App 名称。
- iOS 应用包名:填写您的 iOS App 包名(建议格式:com.xxxxx.xxxxx)。
- 安卓应用包名:填写您的安卓 App 包名(两者可以保持一致,也可以不一致)。
- 渠道标识符:不是必填项,如果不填写,系统会根据包名自动生成。
- 勾选同意,单击 **确定**。
- 在 **获取 SDK** 页面,勾选一款或多款您需要的 SDK 或者业务包,然后下载对应应用平台的集成资料包。(我除了必选的智能生活APP SDK,还选了IPC SDK。下载后发现android的sdk是通过build.gradle配置的,下载的只是配置信息,所以直接参照教程配置就行。)
- 单击 **获取密码**,获取 SDK 的 AppKey,AppSecret,安全图片等信息。
- 如果是安卓端应用,请配置 SHA256 密钥。关于如何获取 SHA256 密钥,请参考 [如何获取SHA密钥](https://developer.tuya.com/cn/docs/app-development/iot_app_sdk_core_sha1?id=Kao7c7b139vrh)。如果有生成签名文件xx.jks,运行cmd切换到签名文件目录下,输入`keytool -list -v -keystore xx.jks`。如果没有生成签名文件,默认debug运行使用的是`C:\Users\(用户名)\.android`目录下的debug.keystore,运行cmd切换到keystore目录下,输入`keytool -list -v -keystore debug.keystore`,输入密钥口令`android`。
# 二、将安卓版涂鸦 **智能生活 App SDK** 集成到您的开发环境中
- https://developer.tuya.com/cn/docs/app-development/integrated?id=Ka69nt96cw0uj
- 在安卓项目的 build.gradle 文件里,添加集成准备中下载的 dependencies 依赖库
```
android {
defaultConfig {
ndk {
abiFilters "armeabi-v7a", "arm64-v8a"
}
}
packagingOptions {
pickFirst 'lib/*/libc++_shared.so' // 多个 AAR(Android Library)文件中存在此 .so 文件,请选择第一个
}
}
dependencies {
implementation 'com.alibaba:fastjson:1.1.67.android'
implementation 'com.squareup.okhttp3:okhttp-urlconnection:3.14.9'
// App SDK 最新稳定安卓版:
implementation 'com.tuya.smart:tuyasmart:3.32.5'
}
```
- 在根目录的 build.gradle 文件,中增加涂鸦 IoT Maven 仓库地址,进行仓库配置。
```
repositories {
jcenter()
google()
// 涂鸦 IoT 仓库地址
maven {
url "https://maven-other.tuya.com/repository/maven-releases/"
}
}
```
- 集成安全图片和设置 Appkey 和 AppSecret
- 在 [涂鸦 IoT 平台](https://iot.tuya.com/oem/sdkList),找到您创建的 SDK。
- 在 **获取密钥** 中,点击 **下载安全图片** > **安全图片下载** 下载安全图片。
- 将下载的安全图片命名为 `t_s.bmp`,放置到工程目录的 `assets` 文件夹下。
- 返回安卓项目,在 `AndroidManifest.xml` 文件里配置 appkey 和 appSecret,在配置相应的权限等。
```
```
- 在 `proguard-rules.pro` 文件配置相应混淆配置。
```
#fastJson
-keep class com.alibaba.fastjson.**{*;}
-dontwarn com.alibaba.fastjson.**
#mqtt
-keep class com.tuya.smart.mqttclient.mqttv3.** { *; }
-dontwarn com.tuya.smart.mqttclient.mqttv3.**
#OkHttp3
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**
-keep class okio.** { *; }
-dontwarn okio.**
-keep class com.tuya.**{*;}
-dontwarn com.tuya.**
```
- 您需要在 `Application` 的主线程中初始化 SDK,确保所有进程都能初始化。示例代码如下。(在AndroidManifest文件中指定自定义的Application)
```
public class TuyaSmartApp extends Application {
@Override
public void onCreate() {
super.onCreate();
TuyaHomeSdk.init(this);
}
}
```
- 在退出应用的时候,调用以下接口可以注销云连接。
```
TuyaHomeSdk.onDestroy();
```
- 开启或关闭日志
- 在 **debug** 模式下,您可以开启 SDK 的日志开关,查看更多的日志信息,帮助您快速定位问题。
- 在 **release** 模式下,建议关闭日志开关。
```
TuyaHomeSdk.setDebugMode(true);
```
- 在安卓项目的 `build.gradle` 文件里,剔除tuyamsmart-log 依赖库。
```
implementation("com.tuya.smart:tuyasmart:3.32.5", {
exclude group: 'com.tuya.smart', module: 'tuyasmart-log'
})
```
-
# 三、快速集成安卓版 IPC SDK
- https://developer.tuya.com/cn/docs/app-development/preparation?id=Ka8j28bt6i7eo
- IPC SDK 依赖于涂鸦 **智能生活 App SDK**,基于此基础上进行拓展开发。
- `app` 目录下的 build.gradle 配置,编译时报错Could not find com.facebook.fresco:fresco:2.2.0.查看https://repo.maven.apache.org/maven2/com/facebook/fresco/fresco/发现没有2.2.0版本但是有2.3.0版本的,所以提前下载2.3.0版本的fresco。后来发现是工程的buidl.gradle注释了jcenter(),maven库没有2.2.0版本的fresco,但是jcenter库有。
```
dependencies {
...
//implementation 'com.facebook.fresco:fresco:2.3.0'
implementation 'com.tuya.smart:tuyasmart-ipcsdk:3.32.5'
...
}
```
- 在 AndroidManifest.xml 文件里配置相应的权限
```
```
- 在 `proguard-rules.pro` 文件配置相应混淆配置
```
-keep class com.tuyasmart.**{*;}
-dontwarn com.tuyasmart.**
```
# 四、用户账户
- https://developer.tuya.com/cn/docs/app-development/tutorial-for-android-final?id=Kapicb0k79vrg
- demo代码地址:https://github.com/tuya/tuya-home-android-sdk-sample-java
- 由于 IPC SDK 依赖于 **智能生活 App SDK**,您需要首先实现如创建账号、添加家庭的操作后才能借助 IPC SDK 实现摄像机相关功能。
- 用户注册和登录等操作可以参考网页教程,也可以参考demo代码tuya-home-android-sdk-sample-java\homesdk-sample\app\src\main\java\com\tuya\appsdk\sample\user
- 获取用户登录状态。TuyaHomeSdk.getUserInstance().isLogin()可以判断用户是否登录。TuyaHomeSdk.getUserInstance().getUser()在用户登录状态下可以获取用户信息。
- 注册用户账号
- 您需要提供 `countryCode` 参数来区分注册地区,用于就近选择涂鸦 IoT 平台的可用区。如中国大陆为 `86`,美国为 `1`
- 您将频繁地调用对象 `User`。它存储了当前用户的所有信息,通过 `TuyaHomeSdk.getUserInstance()` 获取到 `ITuyaUser`,它提供了相关的登录注册方法。详情请参考 [User](https://developer.tuya.com/cn/docs/app-development/usermanage?id=Ka69qtzy9l8nc#title-1-User)。
- 为了加强用户信息的数据安全,涂鸦优化验证码和添加了账号限制。只有验证码服务可用的地区,才可以发送验证码。您需要先查询您的开发者账号是否已经在从涂鸦 IoT 平台开通验证码服务的可用地区列表。返回值 whiteList 表示一个或多个国家或地区,以 , 隔开取值,例如 86 和 01。有关可用区编号列表,请参考 涂鸦云平台介绍。
```
TuyaHomeSdk.getUserInstance().getWhiteListWhoCanSendMobileCodeSuccess(new IWhiteListCallback() {
@Override
public void onSuccess(WhiteList whiteList) {
Toast.makeText(mContext, "Whitelist Success:" + whiteList.getCountryCodes(), Toast.LENGTH_SHORT).show();
}
@Override
public void onError(String code, String error) {
Toast.makeText(mContext, "code: " + code + "error:" + error, Toast.LENGTH_SHORT).show();
}
});
```
- 与大部分注册流程类似,用户必须先获取验证码。无论是手机或是邮箱,您都可以使用统一的获取验证码接口在注册及后续密码修改,验证码登录,信息完善等操作中获取相应的验证码。
```
TuyaHomeSdk.getUserInstance().sendVerifyCodeWithUserName(String userName, String region, String countryCode, int type, IResultCallback callback);
userName 手机号码/邮箱账号
region 区域,默认填写:“” 即可
countryCode 手机区号:如 “86”
callback 回调
type 发送验证码类型:
1:注册
2:登录
3:重置密码
```
- 支持邮箱和手机注册
```
IRegisterCallback callback = new IRegisterCallback() {
@Override
public void onSuccess(User user) {
Toast.makeText(mContext,
"Register success",
Toast.LENGTH_SHORT).show();
}
@Override
public void onError(String code, String error) {
Toast.makeText(mContext,
"code: " + code + "error:" + error,
Toast.LENGTH_SHORT).show();
}
};
if (isEmail){
TuyaHomeSdk.getUserInstance().registerAccountWithEmail(
country_code,
account,
password,
verify_code,
callback
);
}else {
TuyaHomeSdk.getUserInstance().registerAccountWithPhone(
country_code,
account,
password,
verify_code,
callback
);
}
```
- 登录账号。在注册成功后,您就可以使用国家码、手机号和密码登录账号。也可以用邮箱进行登录。
```
TuyaHomeSdk.getUserInstance().loginWithPhonePassword("86", "13666666666", "123456", new ILoginCallback() {
@Override
public void onSuccess(User user) {
Toast.makeText(mContext, "Login success:" +TuyaHomeSdk.getUserInstance().getUser().getUsername(), Toast.LENGTH_SHORT).show();
}
@Override
public void onError(String code, String error) {
Toast.makeText(mContext, "code: " + code + "error:" + error, Toast.LENGTH_SHORT).show();
}
});
TuyaHomeSdk.getUserInstance().loginWithEmail("86", "13666666666@qq.com", "123456", new ILoginCallback() {
@Override
public void onSuccess(User user) {
Toast.makeText(mContext, "Login success:" +TuyaHomeSdk.getUserInstance().getUser().getUsername(), Toast.LENGTH_SHORT).show();
}
@Override
public void onError(String code, String error) {
Toast.makeText(mContext, "code: " + code + "error:" + error, Toast.LENGTH_SHORT).show();
}
});
```
- 由于一些异常或者在一段时间(例如 45 天)不操作账号,用户登录的会话(Session)由会失效掉。同时,修改账号密码、注销用户账户后,也会产生会话过期。这时候需要您引导用户退出应用,重新登录,从而获取 Session。
- 建议您在 Application 中注册会话过期的监听。一旦出现此类回调,请跳转到主页或登录页面,让用户重新登录
- 为activity开启新的栈,Intent.FLAG_ACTIVITY_NEW_TASK 设置状态,首先查找是否存在和被启动的Activity具有相同的任务栈,如果有则直接把这个栈整体移到前台,并保持栈中的状态不变,既栈中的activity顺序不变,如果没有,则新建一个栈来存放被启动的Activity
```
TuyaHomeSdk.setOnNeedLoginListener(new INeedLoginListener() {
@Override
public void onNeedLogin(Context context) {
Intent intent = new Intent(TuyaSmartApp.this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
});
```
- 账号退出
```
TuyaHomeSdk.getUserInstance().touristLogOut(new ILogoutCallback() {
@Override
public void onSuccess() {
}
@Override
public void onError(String code, String error) {
}
});
```
- 注销账号后,一周后才会永久注销并删除用户账户中的所有信息。但是,如果用户在永久注销之前重新登录的话,则注销请求会被取消。
```
TuyaHomeSdk.getUserInstance().cancelAccount(new IResultCallback() {
@Override
public void onError(String code, String error) {
}
@Override
public void onSuccess() {
}
});
```
# 五、家庭管理
- https://developer.tuya.com/cn/docs/app-development/tutorial-for-android-final?id=Kapicb0k79vrg#title-7-%E7%AC%AC%E4%BA%8C%E6%AD%A5%EF%BC%9A%E5%88%9B%E5%BB%BA%E5%92%8C%E7%AE%A1%E7%90%86%E5%AE%B6%E5%BA%AD
- https://developer.tuya.com/cn/docs/app-development/familyrelations?id=Ka6ki8h2c2yo5
- 家庭是智能生活 App SDK 开发下实际场景的最大单位。IoT 设备的添加、编辑、移除、状态变化的监听基于家庭下。您可以在用户账号下创建任意多个家庭。在指定家庭下,您还可以添加并管理多个房间和用户成员。
- 创建家庭
- 在登录状态下您可以创建家庭。之后对于房间,成员,设备等对象的管理都基于家庭下。创建家庭需要通过 `ITuyaHomeManager` 调用 [创建家庭](https://developer.tuya.com/cn/docs/app-development/familyrelations?id=Ka6ki8h2c2yo5#title-1-创建家庭) 接口。
```
TuyaHomeSdk.getHomeManagerInstance().createHome(name, lon, lat, geoName, rooms, new ITuyaHomeResultCallback() {
@Override
public void onSuccess(HomeBean bean) {
// do something
}
@Override
public void onError(String errorCode, String errorMsg) {
// do something
}
});
name 家庭名称,最多支持 25 个字符
lon 经度,如果不设置家庭位置信息,请设置为 0
lat 纬度,如果不设置家庭位置信息,请设置为 0
geoName 家庭地理位置名称
rooms 房间列表
callback 查询结果的回调
```
- 查询家庭列表,在登录状态下您可以直接获取家庭列表。如果您还没创建过家庭,将返回 空数组。
```
TuyaHomeSdk.getHomeManagerInstance().queryHomeList(new ITuyaGetHomeListCallback() {
@Override
public void onSuccess(List homeBeans) {
// do something
}
@Override
public void onError(String errorCode, String error) {
// do something
}
});
```
- 获取家庭详细信息
- 在获取家庭列表或通过 `HomeId` 初始化得到的 `ITuyaHomeManager` 的 `HomeBean` 中 `roomList`、`deviceList`、`groupList`、`sharedDeviceList`、`sharedGroupList` 等属性均为空。您需要调用 [家庭的详细信息](https://developer.tuya.com/cn/docs/app-development/familyrelations?id=Ka6ki8h2c2yo5#title-7-查询家庭详细信息) 接口获取上述属性的具体内容。
```
TuyaHomeSdk.newHomeInstance(10000).getHomeDetail(new ITuyaHomeResultCallback() {
@Override
public void onSuccess(HomeBean bean) {
// do something
}
@Override
public void onError(String errorCode, String errorMsg) {
// do something
}
});
```
- 在创建了家庭后,后续房间成员、用户配网等相关操作均需要基于某一特定家庭。因此建议您将这一特定家庭作为 App 的全局变量存储。当然,您也可以在本地随时切换当前家庭,涂鸦 IoT 平台并不会记录这一信息。在本教程和配套的 Sample 中,默认将列表的第一个家庭设为当前家庭。
# 六、设备配网
- https://developer.tuya.com/cn/docs/app-development/tutorial-for-android-final?id=Kapicb0k79vrg#title-12-%E7%AC%AC%E4%B8%89%E6%AD%A5%EF%BC%9A%E8%AE%BE%E5%A4%87%E9%85%8D%E7%BD%91
- https://developer.tuya.com/cn/docs/app-development/wifinetwork?id=Ka6ki8lbwu82c
- 配网方式介绍
- 本模块以 Wi-Fi 配网为例介绍如何使用 SDK 将设备配置到云端。Wi-Fi 配网方式包括 [快连模式(EZ)](https://developer.tuya.com/cn/docs/app-development/quick-connection-mode?id=Kaixju76a5iq9)、[热点模式(AP)](https://developer.tuya.com/cn/docs/app-development/hotspot-mode?id=Kaixk6wxla1oy)、扫 App 二维码三种方式。
- 相比 EZ 模式,AP 模式成功率高、可靠性好,对手机和路由器有兼容性要求小。配网成功率高于 EZ 模式。因此,在之后的版本中,我们推荐使用 **AP 模式** 代替 **EZ 模式**。
- 获取配网 Token
- 始配网之前,SDK 需要在联网状态下从涂鸦云获取配网 Token,然后才可以开始热点模式配网。Token 的有效期为 10 分钟,且配置成功后就会失效,再次配网需要重新获取。获取 Token 需要上传当前的 `homeId`,因此您需要确保账号处于登录状态并至少创建了一个家庭。
```
TuyaHomeSdk.getActivatorInstance().getActivatorToken(homeId,
new ITuyaActivatorGetToken() {
@Override
public void onSuccess(String token) {
}
@Override
public void onFailure(String s, String s1) {
}
});
```
- 摄像头二维码配网
- 通过摄像头设备扫描 App 二维码来传递配网信息的方式来实现配网设备。
- 配网流程图

- 初始化配网参数,获取配网实现类ITuyaCameraDevActivator。`timeout` 单位为秒,默认为 100,您可以设置为任意值。但不建议将此值设置得过小,否则将影响配网结果。
```
TuyaCameraActivatorBuilder builder = new TuyaCameraActivatorBuilder()
.setContext(context)
.setSsid(ssid)
.setPassword(password)
.setToken(token)
.setTimeOut(timeout)
.setListener(new ITuyaSmartCameraActivatorListener() {
@Override
public void onQRCodeSuccess(String qrcodeUrl) {
//返回生成二维码的 URL 链接
}
@Override
public void onError(String errorCode, String errorMsg) {
//配网失败
}
@Override
public void onActiveSuccess(DeviceBean devResp) {
//配网成功
}
});
ITuyaCameraDevActivator mTuyaActivator = TuyaHomeSdk.getActivatorInstance().newCameraDevActivator(builder);
```
- 生成二维码图给摄像头配网。mTuyaActivator根据前面配网信息调用createQRCode方法生成配网URL字符串,通过 onQRCodeSuccess 回调返回。配置Zxing库,在onQRCodeSuccess方法中根据URL字符串生成二维码图显示到手机上。用户用手机显示的二维码图放到摄像头前面进行识别联网。
```
//build.gradle配置zxing
implementation 'com.google.zxing:core:3.2.1'
mTuyaActivator.createQRCode(); //通过 onQRCodeSuccess 回调返回
@Override
public void onQRCodeSuccess(String qrcodeUrl) {
//返回生成二维码的 URL 链接
Bitmap bitmap = createQRCode(qrcodeUrl,300);
//显示bitmap
...
}
public static Bitmap createQRCode(String url, int widthAndHeight)
throws WriterException {
Hashtable hints = new Hashtable();
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
hints.put(EncodeHintType.MARGIN,0);
BitMatrix matrix = new MultiFormatWriter().encode(url,
BarcodeFormat.QR_CODE, widthAndHeight, widthAndHeight, hints);
int width = matrix.getWidth();
int height = matrix.getHeight();
int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
if (matrix.get(x, y)) {
pixels[y * width + x] = BLACK;
}
}
}
Bitmap bitmap = Bitmap.createBitmap(width, height,
Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
return bitmap;
}
```
- 开始配网
```
mTuyaActivator.start();
```
- 停止配网
```
mTuyaActivator.stop();
```
- 销毁数据
```
mTuyaActivator.onDestory();
```
# 七、设备管理
- https://developer.tuya.com/cn/docs/app-development/devicemanage?id=Ka6ki8r2rfiuu
- https://developer.tuya.com/cn/docs/app-development/tutorial-for-android-final?id=Kapicb0k79vrg#title-17-%E7%AC%AC%E5%9B%9B%E6%AD%A5%EF%BC%9A%E6%8E%A7%E5%88%B6%E8%AE%BE%E5%A4%87
- 获取设备列表和设备详细信息,在对应的家庭下调用 [获取家庭详细信息](https://developer.tuya.com/cn/docs/app-development/familyrelations?id=Ka6ki8h2c2yo5#title-7-查询家庭详细信息) 接口,在 [`HomeBean`](https://developer.tuya.com/cn/docs/app-development/familyrelations?id=Ka6ki8h2c2yo5#title-0-功能说明) 的 `getDeviceList` 中查看对应的设备列表。
```
List deviceList = homeBean.getDeviceList();
DeviceBean 设备信息数据类型:
属性 类型 说明
devId String 设备 ID。
name String 设备名称。
iconUrl String 图标地址。
schema String 设备控制数据点的类型信息。
productId String 产品 ID,同一个产品 ID 的设备的 schema 信息一致。
timezoneId String 设备所在的时区。
category String (已弃用)设备类型。如果您需要获取设备类型,请使用产品信息的category字段。
pv String 网关协议版本。
bv String 网关通用固件版本。
time Long 设备激活时间。
schemaMap Map schema 的缓存数据。
dps Map 设备当前数据信息。key 是 DP ID,value 是取值。详情请参考 设备功能点 章节。
getIsOnline Boolean 设备是否在线,指局域网或者云端在线。
isLocalOnline Boolean 设备的局域网在线状态。
supportGroup Boolean 设备是否支持群组,如果不支持请到 涂鸦 IoT 平台 开启此功能。
isShare Boolean 是否是分享的设备。
virtual Boolean 是否是虚拟设备。
isZigBeeWifi Boolean 是否是 Zigbee 网关设备。
hasZigBee Boolean 是否有 Zigbee 设备。
nodeId String 用于网关和子设备类型的设备。属于子设备的一个属性,标识其短地址 ID,一个网关的子设备的 nodeId 都是唯一的。
meshId String 用于网关和子设备类型的设备。属于子设备的一个属性,标识其网关 ID。
lon String 设备所在经度信息。
lat String 设备所在纬度信息。
productBean ProductBean 产品信息。
```
- 初始化设备控制类。根据设备 ID 初始化设备控制类 `ITuyaDevice`。设备进行监听和控制等操作需要使用`ITuyaDevice`。
```
ITuyaDevice mDevice = TuyaHomeSdk.newDeviceInstance(deviceBean.getDevId());
```
- 监听设备。ITuyaDevice 提供设备相关信息的监听,包含:DP 数据、设备名称、设备在线状态和设备移除。注意:是用registerDevListener而不是registerDeviceListener。
```
ITuyaDevice.registerDevListener(IDevListener listener);
public interface IDevListener {
/**
* DP 数据更新
*
* @param devId 设备 ID
* @param dpStr 设备发生变动的功能点,为 JSON 字符串,数据格式:{"101": true}
*/
void onDpUpdate(String devId, String dpStr);
/**
* 设备移除回调
*
* @param devId 设备id
*/
void onRemoved(String devId);
/**
* 设备上下线回调。如果设备断电或断网,服务端将会在3分钟后回调到此方法。
*
* @param devId 设备 ID
* @param online 是否在线,在线为 true
*/
void onStatusChanged(String devId, boolean online);
/**
* 网络状态发生变动时的回调
*
* @param devId 设备 ID
* @param status 网络状态是否可用,可用为 true
*/
void onNetworkStatusChanged(String devId, boolean status);
/**
* 设备信息更新回调
*
* @param devId 设备 ID
*/
void onDevInfoUpdate(String devId);
}
```
- 取消设备监听。
```
ITuyaDevice.unRegisterDevListener()
```
- 设备控制
- https://developer.tuya.com/cn/docs/app-development/andoird_device_control?id=Kaixh4pfm8f0y
- [`DeviceBean`](https://developer.tuya.com/cn/docs/app-development/devicemanage?id=Ka6ki8r2rfiuu#title-0-功能说明) 类的 `dps` 属性定义了设备的状态,称作数据点(DP,Data Point)或功能点。
- `dps` 数组里,每个 `key` 对应一个设备功能的 `dpId`,`dpValue` 为该设备功能的值。
一款产品的设备功能定义可以在 [涂鸦 IoT 平台](https://iot.tuya.com/pmg/list) 上查看。如下图:

- 设备的功能点信息存放在 `dps` 中。`dps` 存放了该设备的所有功能点信息,每个功能点被封装成一个键值对。
```
Map dps = TuyaHomeSdk.getDataInstance().getDps(deviceBean.getDevId());
```
- 查询单个 DP 数据,查询后的数据会通过 `IDevListener.onDpUpdate()` 接口进行异步回调。该接口主要是针对不主动发送数据的设备 DP,例如倒计时信息查询。常规查询 DP 数据值时,可通过 `DeviceBean` 中的 `getDps()` 获取。
```
mDevice.getDp(dpId, new IResultCallback() {
@Override
public void onError(String code, String error) {
}
@Override
public void onSuccess() {
}
});
```
- 接口向设备发送功能点,改变设备状态或功能,来达到设备控制的目的。参数 `dps` 中可以包含多个功能点,您可以一次同时改变设备的多个状态。
```
//发送控制命令时,请注意数据类型。例如:
//设备功能的数据类型是数值型( value )时,则发送的应该是 {"104": 25} 而不是 {"104": "25"}。
//透传类型传输的 Byte 数组是 16 进制字符串格式,并且必须是偶数位,则发送的应该是 {"105": "0110"} 而不是 {"105": "110"}。
// 设置 dpId 为 101 的布尔型设备功能示例,作用:开关打开
dps = {"101": true};
// 设置 dpId 为 102 的字符串型设备功能示例,作用:设置 RGB 颜色为 ff5500
dps = {"102": "ff5500"};
// 设置 dpId 为 103 的枚举型设备功能示例,作用:设置档位为 2 档
dps = {"103": "2"};
// 设置 dpId 为 104 的数值型设备功能示例,作用:设置温度为 20°
dps = {"104": 20};
// 设置 dpId 为 105 的透传型(byte 数组)设备功能示例,作用:透传红外数据为 1122
dps = {"105": "1122"};
// 多个功能合并发送
dps = {"101": true, "102": "ff5500"};
mDevice.publishDps(dps, new IResultCallback() {
@Override
public void onError(String code, String error) {
// 错误码 11001 有下面几种原因:
//1:数据类型发送格式错误,例如,String 类型格式发成 Boolean 类型数据。
//2:不能下发只读类型 DP 数据,参考 SchemaBean getMode,"ro" 是只读类型。
//3:Raw 格式数据发送的不是 16 进制字符串。
}
@Override
public void onSuccess() {
}
});
//发送控制指令格式
{
"(dpId)":"(dpValue)"
}
//局域网控制
ITuyaDevice.publishDps(dps, TYDevicePublishModeEnum.TYDevicePublishModeLocal, callback);
//云端控制
ITuyaDevice.publishDps(dps, TYDevicePublishModeEnum.TYDevicePublishModeInternet, callback);
//自动控制
ITuyaDevice.publishDps(dps, TYDevicePublishModeEnum.TYDevicePublishModeAuto, callback);
//或者
ITuyaDevice.publishDps(dps, callback);
//指定通道控制
ITuyaDevice.publishDps(dps, orders, callback);
参数 说明
dps 设备功能,全称为 data points,通过 JSON 字符串的格式表示,详情请参考 设备功能 章节
publishModeEnum 设备控制方式
callback 返回控制指令是否成功的回调
orders 通道顺序,可以参考 CommunicationEnum 枚举类,例如 [3 , 1] 指定的是 优先蓝牙控制,蓝牙不在线则选择云端控制
```
- 修改设备名称,重命名成功后,`IDevListener.onDevInfoUpdate()` 会收到通知。调用方法`TuyaHomeSdk.getDataInstance().getDeviceBean(String devId)`可以获取最新数据,然后刷新设备信息即可。
```
mDevice.renameDevice("设备名称", new IResultCallback() {
@Override
public void onError(String code, String error) {
// 修改设备名称失败
}
@Override
public void onSuccess() {
// 修改设备名称成功
}
});
```
- 移除设备,从用户设备列表中移除设备
```
mDevice.removeDevice(new IResultCallback() {
@Override
public void onError(String errorCode, String errorMsg) {
}
@Override
public void onSuccess() {
}
});
```
- 恢复出厂设置。设备恢复出厂设置后,设备的相关数据会被清除掉,并重新进入待配网状态。如果是 Wi-Fi 设备,默认进入 Wi-Fi 快连模式。
```
mDevice.resetFactory(new IResultCallback() {
@Override
public void onError(String errorCode, String errorMsg) {
}
@Override
public void onSuccess() {
}
});
```
- 查询设备 Wi-Fi 的信号强度
```
mDevice.requestWifiSignal(new WifiSignalListener() {
@Override
public void onSignalValueFind(String signal) {
}
@Override
public void onError(String errorCode, String errorMsg) {
}
});
```
- IPC 存储卡管理
- https://developer.tuya.com/cn/docs/app-development/sdcard?id=Ka6nxw2eufia3
- 在开始管理存储卡或者进行录像回放前,需要先获取存储卡的状态,如果设备未检测到存储卡,则无法进行下一步。如果存储卡异常,则需要先格式化存储卡。获取存储卡状态的功能点 ID 是 “110”。
```
值 描述
1 正常
2 异常( SD 卡损坏或格式不对)
3 空间不足
4 正在格式化
5 无 SD 卡
注意:该 DP 点下发不需要带参数,设置为 null 值即可。
```
- 格式化。在格式化存储卡的时候,根据摄像机厂商的实现,有两种情况。有些厂商实现的固件中,会主动上报格式化的进度,格式化完成后也会主动上报当前的容量状态,但是有少部分厂商的固件,不会主动上报,所以需要定时主动去查询格式化的进度,当进度达到 100 时,再主动去查询当前的容量状态。
格式化的功能点 ID 是 “111”, 下发该 DP 点可以启动格式化操作
- 查询格式化状态的功能点 ID 是 “117”, 下发该 DP 点可以查询格式化进度,当进度达到100时,即格式化结束。可以再次去查询存储卡容量。
- 查询存储卡容量的功能点 ID 是 “109”,下发该 DP 点可以获取到涂鸦 IPC 摄像机当前的存储卡容量。存储卡容量值的字符串格式:总容量|已使用容量|空闲容量,单位`KB`。
- 开关存储卡录像的功能点 ID 是 “150”,下发该 DP 点可以控制涂鸦智能摄像机是否开启录像功能。
- 涂鸦摄像机在插入存储卡后,可以将采集的影像录制保存在存储卡中,可以通过 SDK 设置视频录制开关和模式。录制模式分为以下两种:
- **连续录制**:摄像机会将采集到的音视频连续不断的录制保存在存储卡中,存储卡的容量不足的时候,将会覆盖最早录制的视频数据。
- **事件录制**:摄像机只会在触发侦测报警的时候才会开始录制视频,视频的长短会根据事件类型,和事件持续时间而变化。
修改存储卡录像模式的功能点 ID 是 “151”。
```
值 描述
1 事件录像(检测到移动再录像到 SD 卡)
2 连续录像
```
- 回收设备资源。应用或者 Activity 关闭时,可以调用此接口,回收设备占用的资源。
```
mDevice.onDestroy();
```
# 八、IPC SDK
- https://developer.tuya.com/cn/docs/app-development/architecure?id=Ka6nuvwrlt53p
- **IPC SDK** 基于涂鸦 **智能生活 App SDK** 封装了智能摄像机的相关功能。IPC(IP Camera)全称为网络摄像机,由网络编码模块和模拟摄像机组合而成。
- 判断是否是智能摄像机。根据设备 id 来判断是否是智能摄像机设备,如果是智能摄像机设备,则可以根据 DeviceBean 中的信息来创建摄像机对象。
```
ITuyaIPCCore cameraInstance = TuyaIPCSdk.getCameraInstance();
if (cameraInstance != null) {
cameraInstance.isIPCDevice(devId)
}
```
- 涂鸦智能摄像机支持三种 p2p 通道实现方案,IPC SDK 会根据 p2p 类型来初始化不同的摄像机具体实现的对象,通过下面的方式获取设备的 p2p 类型。
```
ITuyaIPCCore cameraInstance = TuyaIPCSdk.getCameraInstance();
if (cameraInstance != null) {
cameraInstance.getP2PType(devId)
}
```
- 创建 ITuyaSmartCameraP2P 对象。视频直播需要创建 `ITuyaSmartCameraP2P` 对象,然后进行 P2P 连接,连接成功后就可以播放实时视频、截图、录制视频和实时对讲数据传输。
```
ITuyaIPCCore cameraInstance = TuyaIPCSdk.getCameraInstance();
if (cameraInstance != null) {
ITuyaSmartCameraP2P mCameraP2P = cameraInstance.createCameraP2P(devId));
}
```
- 视频直播
- 流程图

- 页面布局文件中引入渲染视图容器。`TuyaCameraView` 是 IPC SDK 提供的视频渲染视图,如果您需要使用自己的视频渲染视图,只需要实现 `IRegistorIOTCListener` 接口,再将您自己的视频渲染视图和 `ITuyaSmartCameraP2P` 绑定即可。
```
```
- 为渲染视图容器设置回调。`AbsVideoViewCallback`,渲染视图回调抽象类,开发者只需要重写自己关心的回调,一般只需要重写 `onCreated` 方法。
```
TuyaCameraView mVideoView = findViewById(R.id.camera_video_view);
mVideoView.setViewCallback(new AbsVideoViewCallback() {
@Override
public void onCreated(Object view) {
super.onCreated(view);
//渲染视图构造完成时回调
}
});
//渲染视图构造完成时回调。
public void onCreated(Object view);
//点击视图时回调。
public void videoViewClick();
//触发视图滑动操作时回调。
//该接口仅支持 p2p 1.0 的设备,其他设备使用 setOnRenderDirectionCallback 接口。滑动方向。"0"代表上,"2"代表右,"4"代表下,"6"代表左
public void startCameraMove(String cameraDirection);
//点击视图后手指抬起时回调
public void onActionUP();
```
- 构造渲染视图。调试运行时遇到闪退,报错java.lang.NoClassDefFoundError: Failed resolution of: Lkotlin/jvm/internal/Intrinsics,定位到createVideoView方法。网上找了原因,是JAVA 调用Kotlin 第三方包时出错。参考网址https://jingyan.baidu.com/article/48206aeae9891e606ad6b395.html
```
TuyaCameraView mVideoView = findViewById(R.id.camera_video_view);
mVideoView.createVideoView(devId);
//顶层build.gradle添加
ext.kotlin_version = "1.3.72"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
//应用moudle 添加
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.3.72"
```
- 获取视频渲染视图。需要注意如果未构造视频渲染视图会返回 null。
```
TuyaCameraView mVideoView = findViewById(R.id.camera_video_view);
mVideoView.createdView();
```
- 为 ITuyaSmartCameraP2P 绑定渲染视图
```
mCameraP2P.generateCameraView(mVideoView.createdView());
```
- 注册 P2P 监听。向 ITuyaSmartCameraP2P 注册监听器,否则无法正常显示画面。
```
void registerP2PCameraListener(AbsP2pCameraListener listener);
```
- P2P 连接。在开始视频播放之前,需要先连接 P2P 通道。P2P 状态需要使用者自己维护,SDK 只负责下发指令和接收摄像机响应结果。
```
mCameraP2P.connect(devId, new OperationDelegateCallBack() {
@Override
public void onSuccess(int sessionId, int requestId, String data) {
//连接成功
}
@Override
public void onFailure(int sessionId, int requestId, int errCode) {
//连接失败
}
});
//指定优先连接方式
void connect(String devId, int mode, OperationDelegateCallBack callBack);
0 自动选择
1 外网连接优先
2 局域网连接优先
```
- 断开 P2P 通道
```
void disconnect(String devId, OperationDelegateCallBack callBack);
```
- 实时播放视频。P2P连接成功之后,就能进行实时视频播放了。startPreview 成功回调之后,onReceiveFrameYUVData 回调会开始接收视频数据,并抛给业务层。
```
void startPreview(int clarity, OperationDelegateCallBack callBack);
clarity 清晰度模式
模式 值
标清 2
高清 4
```
- 停止播放实时视频
```
int stopPreview(OperationDelegateCallBack callBack);
```
- 存储卡回放
- 涂鸦 IPC 摄像机支持 SD卡录制功能,智能摄像机插入存储卡后,可以查看存储卡的信息和状态,并设置录像开关和模式,详情可以参考 [存储卡管理功能](https://developer.tuya.com/cn/docs/app-development/android-app-sdk/extension-sdk/ipc-sdk/extension-features/sdcard?id=Ka6nxw2eufia3)。
- 流程图

- 设备在存储卡中保存视频录像后,可以通过 IPC SDK 在 App 端播放视频录像,同实时视频直播一样,需要创建 `ITuyaSmartCameraP2P` 对象,连接上 p2p 通道。p2p 通道连接成功后,可以获取到设备端存储卡中录制的视频片段时间信息,然后播放视频片段。设备端保存在存储卡中的视频片段,IPC SDK 支持以天为单位查看和播放视频录像,并提供查询某年某月中,哪几天保存有视频录像,以便于用户查看。
- 需要获取到回放视频记录的信息。首先获取有回放视频记录的日期。
```
int year = Integer.parseInt(substring[0]);
int mouth = Integer.parseInt(substring[1]);
queryDay = Integer.parseInt(substring[2]);
mCameraP2P.queryRecordDaysByMonth(year, mouth, new OperationDelegateCallBack() {
@Override
public void onSuccess(int sessionId, int requestId, String data) {
//data是获取到的月份数据
MonthDays monthDays = JSONObject.parseObject(data, MonthDays.class);
mBackDataMonthCache.put(mCameraP2P.getMonthKey(), monthDays.getDataDays());
mHandler.sendMessage(MessageUtil.getMessage(MSG_DATA_DATE, ARG1_OPERATE_SUCCESS, data));
}
@Override
public void onFailure(int sessionId, int requestId, int errCode) {
mHandler.sendMessage(MessageUtil.getMessage(MSG_DATA_DATE, ARG1_OPERATE_FAIL));
}
});
```
- 获取到有用回放记录的日期后,根据日期获取当日的视频回放记录
```
int year = Integer.parseInt(substring[0]);
int mouth = Integer.parseInt(substring[1]);
int day = Integer.parseInt(substring[2]);
mCameraP2P.queryRecordTimeSliceByDay(year, mouth, day, new OperationDelegateCallBack() {
@Override
public void onSuccess(int sessionId, int requestId, String data) {
//data是获取到指定日期的视频片段数据集
parsePlaybackData(data);
}
@Override
public void onFailure(int sessionId, int requestId, int errCode) {
mHandler.sendEmptyMessage(MSG_DATA_DATE_BY_DAY_FAIL);
}
});
private void parsePlaybackData(Object obj) {
RecordInfoBean recordInfoBean = JSONObject.parseObject(obj.toString(), RecordInfoBean.class);
if (recordInfoBean.getCount() != 0) {
List timePieceBeanList = recordInfoBean.getItems();
if (timePieceBeanList != null && timePieceBeanList.size() != 0) {
mBackDataDayCache.put(mCameraP2P.getDayKey(), timePieceBeanList);
}
mHandler.sendMessage(MessageUtil.getMessage(MSG_DATA_DATE_BY_DAY_SUCC, ARG1_OPERATE_SUCCESS));
} else {
mHandler.sendMessage(MessageUtil.getMessage(MSG_DATA_DATE_BY_DAY_FAIL, ARG1_OPERATE_FAIL));
}
}
RecordInfoBean数据模型:
参数 说明
count 片段个数
List 视频片段集合
TimePieceBean数据模型:
参数 说明
startTime 视频片段开始时间
endTime 视频片段结束时间
playTime 视频片段播放时间
```
- 开启回放
```
void startPlayBack(int startTime, int stopTime, int playTime, OperationDelegateCallBack callBack, OperationDelegateCallBack finishCallBack);
参数 说明
startTime 开始时间(10 位时间戳)
stopTime 结束时间
playTime 播放时间
callBack 开启回放回调
finishcallBack 结束回放回调
```
- 暂停回放
```
void pausePlayBack(OperationDelegateCallBack callBack);
```
- 恢复回放
```
void resumePlayBack(OperationDelegateCallBack callBack);
```
- 结束回放
```
void stopPlayBack(OperationDelegateCallBack callBack);
```
- 倍速播放。需要设备支持,可以使用[设备能力类ICameraConfigInfo][https://developer.tuya.com/cn/docs/app-development/android-app-sdk/extension-sdk/ipc-sdk/camera-functions/avfunction]获取设备支持的播放倍速。
```
void setPlayBackSpeed(int speed, OperationDelegateCallBack callBack);
参数 说明
speed 播放倍速,指定的播放倍数需要设备支持
callBack 操作回调
```
- 开启回放成功后,可以使用音视频功能,例如开启/停止视频录制、视频截图、开启/关闭视频声音,详情可参考 [音视频功能](https://developer.tuya.com/cn/docs/app-development/android-app-sdk/extension-sdk/ipc-sdk/camera-functions/avfunction?id=Ka6nuvucjujar)。
-
- 销毁 ITuyaSmartCameraP2P 对象。不再使用 camera 功能的时候,一定要注销 P2P 监听器、销毁 P2P 对象。
```
@Override
public void onDestroy() {
if (null != mCameraP2P) {
mCameraP2P.removeOnP2PCameraListener();
mCameraP2P.destroyP2P();
}
}
```
# 附录1:Android - 文件系统与Android11 分区存储
##### *认识Android文件系统
##### *了解分区存储
##### *分区存储的适配
## 一、Android文件系统
Android文件系统分为 **内部存储(internal storage)** 和**外部存储(external storage)**
1.1 用一个表格来直观对比一下两者:

storage.png
1.2 应用的私有路径
应用在安装之后,系统会自动在内部存储和外部存储,分别建立应用的私有存储区域。
内部存储 : data/user/0/packageName
外部存储 : storage/emulated/0/android/data/packageName
当应用卸载或者清除数据后,该区域文件会被删除。
1.3 内外部存储图解

storage2.png
## 二、 了解分区存储
Android 10版本中,Google推出 **分区存储**(scoped storage)的功能。
**背景**:
分区存储功能是针对内置的外部存储来说的,很多应用喜欢在外部存储的根目录创建自己的文件夹,比如:storage/emulated/0/***
这样做的好处:1. 当不断向该目录存储时,应用自己的容量不会变化; 2. 当应用卸载时,该目录下文件不会被删除,可用于保存一些可持久性的文件。
但是也有坏处: 1. 对用户来说,会有很多垃圾文件存在于手机中;2. 只要获取到Read 和 Write权限,就可以随意访问外部存储的任何目录,信息安全存在隐患。
**分区存储**:
1. 每个应用向自己的私有目录读写文件,不需要读写权限。私有文件目录具体路径: storage/emulated/0/android/data/packageName/ ,获取方法: Context#getExternalFilesDir()
2. 应用即使获取了读写权限,也无法访问其他应用的私有目录。
3. 当应用需要获取媒体文件时,通过 **MediaStore API** 向公共存储目录DCIM、Music或者Movie获取。同样写媒体文件也是如此。并且读写自己的文件时不需要申请权限。 只有读其他应用的媒体文件时才会需要申请READ_EXTERNAL_STORAGE权限。
(更新:Android11为目标平台时,可以使用文件直接路径去访问媒体,这是在Android10上没有的,应用的性能会略有下降,还是推荐使用MediaStore )
4. 当应用需要获取其他非媒体文件时,比如doc、pdf文件,需要使用 **系统的文件选择器SAF** 来进行访问。
5. 所以WRITE_EXTERNAL_STORAGE权限,在未来的Android11版本里,会被废弃。 (写文件不需要权限,只能在私有目录和公共目录写文件)
## 三、分区存储适配
##### 旧版存储位置迁移
除了应用的私有目录和公共目录,其他位置都称为 **旧版存储位置**,我们需要将旧版存储位置的数据迁移到能兼容分区存储的位置。
1. 如果以Android 11为目标平台的应用,需要在manifest清单中标记**preserveLegacyExternalStorage** 为true,这样在Android11的机器上**覆盖安装**时,才能访问旧版存储位置,卸载重装会失效。
2. 如果以Android10为目标平台,覆盖安装可以访问旧版存储,且将manifest清单中标记**requestLegacyExternalStorage** 为true,在Android10机器上**重新安装**也能访问旧版存储位置。在Android11的机器上两种安装方式都会失效,需要加上preserveLegacyExternalStorage = true,且**覆盖安装**才能访问旧版存储位置。卸载重装会失效。
3. 如果以Android 9及以下为目标平台时,就能正常的进行文件移动。将应用在外部存储器根目录的保存的数据中,如果能接受随应用的卸载而删除的文件,迁移至storage/emulated/0/android/data/packageName/目录下。需要和其他应用共享的媒体文件,迁移至媒体存储位置。
##### 正确使用读写API
1. 只在外部存储的应用私有目录下,用直接路径读写文件
2. 访问或者共享媒体文件,使用MediaStore在公共目录下读写文件
3. 访问或者共享非媒体文件,使用系统的文件选择器SAF在公共目录Download下读写文件