# APP Android SDK

介绍

iFLYOS 提供了开源的 APP Android SDK 用于执行对设备的授权及部分远程控制操作。模块中包含了账号模块、绑定模块、接口模块、声波配网及 WebViewBridge 模块。

更新日志

日期 更新内容
20190128 增加直接设置第三方 Token 的方式
20190225 更新媒体相关接口
1.内容发现相关接口修改
2. 我的收藏相关接口修改
3. 控制设备播放接口修改
20190308 更新酷狗绑定相关内容,补充部分接口
20190701 1. 开源配网相关代码,精简 AAR 体积
2. 新增部分接口
3. 开源蓝牙配网文档
20200113 1. 更新部分接口
2. 新增部分可直接调用 H5 的页面
20200306 1. 更新播放接口的返回说明
2. 更新获取用户标识接口说明
20200330 1. 更新喜马拉雅外部打开支持
20200731 1. 支持订阅媒体播放状态、设备在离线状态变更事件
20200804 1. 调整更新设备信息接口
20200901 1. 新增播放记录相关接口
20200911 1. 更新 AP 配网相关开源代码
20200922 1. 精简 SDK
20201022 1. 优化酷狗信源续费相关体验
20201125 新增训练计划(即快捷指令)相关接口
20201208 完善推送 SDK 相关文档说明

# 快速接入

接入要求

设备系统版本 Android 5.1 或以上
Android Studio 3.1.2 或以上

# 导入依赖

这里 (opens new window)下载最新的 SDK demo,将 aar 文件导入到应用模块中作为 libs 引用,build.gradle 中声明

dependencies {
    implementation(name: 'iflyos-app-sdk', ext: 'aar')

    // SDK 依赖项
    implementation 'com.squareup.retrofit2:retrofit:2.4.0'
    implementation 'com.squareup.retrofit2:converter-scalars:2.4.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'
    implementation 'com.google.code.gson:gson:2.8.5'
}

# 声明权限

SDK 模块需要在AndroidManifest.xml中声明以下权限

<uses-permission android:name="android.permission.INTERNET" />

# 混淆规则

在工程的 proguard-project.txt 里添加以下相关规则

-keep class com.iflytek.home.sdk.**{*;}
-keep public class com.iflytek.home.app.R$*{
  public static final int *;
}
-keep class cn.iflyos.open.library.**{*;}
-keepattributes *Annotation*
-keep class com.squareup.okhttp3.**{*;}
-dontwarn okio.**
-keep interface com.squareup.okhttp3.**{*;}
-dontwarn com.squareup.okhttp3.**

# 初始化

在你的应用入口处调用

IFlyHome.init(context, appId, loginWay)

此处为 Kotlin 实现,如果使用 Java 调用,请使用 IFlyHome.INSTANTCE,下文同。

参数 说明 类型
context 上下文对象 Context
appId 用于 SDK 的 app id,请参阅申请方式 String
loginWay 声明 SDK 使用的登录方式。
STANDARD 代表默认的登录方式,通过调用打开网页来登录。
CUSTOM_TOKEN 表示通过设置第三方 Token 的方式,如何使用请联系支持邮箱
enum

# WebViewBridge

SDK 中大部分带 UI 交互页面均为 Web 页面,你可以通过调用接口注册你的 WebView 控件,SDK 会为其绑定 WebViewBridge 及相关定制。

# 注册 WebView

简单地,你可以通过调用以下方法对 WebView 进行注册

val webViewTag = IFlyHome.register(webView, object: IFlyHomeCallback() {
    // 表示网页顶栏主题色更新
    override fun updateHeaderColor(color: String) {}

    // 表示网页标题更新
    override fun updateTitle(title: String) {}

    // 表示网页请求在新 Activity 中打开指定页面
    override fun openNewPage(tag: String) {}

    // 表示网页请求关闭页面
    override fun closePage() {}

    override fun getWebViewClient(): WebViewClient? {}

    override fun getWebChromeClient(): WebChromeClient? {}
})

如果你需要自定义 WebViewClient 或者 WebChromeClient,为了不影响 SDK 对 WebView 的绑定,请在 IFlyHomeCallback 中作为返回值即可,此方法并不要求必须实现。

返回结果中的 webViewTag 可以让你在打开 IFLYOS 网页时传入,告诉 SDK 你想在哪个 WebView 中打开页面。

如果 SDK 通过 openNewPage(tag) 传递了参数 tag,则应当使用以下方式注册 WebView,SDK 将在 WebView 绑定完成后自动跳转到指定的访问页面。

IFlyHome.register(webView, iFlyHomeCallback, tag)

# 打开页面

你可以通过以下方法打开 IFLYOS 提供的指定页面

val result = IFlyHome.openWebPage(webViewTag, pageIndex, params)
// webViewTag 在注册 WebView 时返回
// pageIndex 可用参数:
//   IFlyHome.ACCOUNTS,
//   IFlyHome.CLOCKS,
//   IFlyHome.CONTROLLED_DEVICES,
//   IFlyHome.SKILLS,
//   IFlyHome.PERSONAL_SOUNDS
//   IFlyHome.WAKEUP_WORDS
//   IFlyHome.TIME_TO_SLEEP
//   IFlyHome.SPEAKER
//   IFlyHome.BLUETOOTH
//   IFlyHome.CHECK_UPDATE
//   IFlyHome.DIALOGUE
//   IFlyHome.COMMUNICATION_SETTING
//   IFlyHome.MESSAGE
//   IFlyHome.DEVICE_ZONE
//   IFlyHome.TRAINING_PLAN
// params 可为空,当 pageIndex 为以下参数时,params 中需要传入 deviceId 参数:
//   IFlyHome.WAKEUP_WORDS
//   IFlyHome.SPEAKER
//   IFlyHome.BLUETOOTH
//   IFlyHome.TIME_TO_SLEEP
//   IFlyHome.CHECK_UPDATE
//   IFlyHome.COMMUNICATION_SETTING
//   IFlyHome.MESSAGE
//   IFlyHome.DEVICE_ZONE

参数说明

字段 类型 必须出现 说明
webViewTag String 在注册 WebView 时返回的标识
pageIndex String 含义参考 IFLYOS 提供页面
params Map<String, String> 请求参数

result 含义参考 状态码。 当 pageIndex 为以下参数时, params 中需要传入 deviceId 参数:

  • IFlyHome.WAKEUP_WORDS
  • IFlyHome.SPEAKER
  • IFlyHome.BLUETOOTH
  • IFlyHome.TIME_TO_SLEEP
  • IFlyHome.CHECK_UPDATE

# 打开指定内容账号页面

val result = IFlyHome.openWebAccounts(webViewTag, type)

参数说明

字段 类型 必须出现 说明
webViewTag String 在注册 WebView 时返回的标识
type String 获取内容账号 接口中返回的 bind_type

# 生命周期

当 WebView 可见时应调用

IFlyHome.resumeWebView(webViewTag)

当 WebView 不可见时应调用

IFlyHome.pauseWebView(webViewTag)

# 账号模块

在调用登录后,SDK 内部维护了调用各个接口及打开各种网页所需要的登录状态。

# 登录

目前支持两种登录方式。根据初始化 SDK 时传入的 loginWay,应当选择不同的登录方式。

# STANDARD

val result = IFlyHome.openLogin(object : IFlyHomeLoginCallback {
    // 登录失败回调
    override fun onLoginFailed(type: Int, error: Throwable?) {}

    // 登录成功回调
    override fun onLoginSuccess() {}

    // SDK 期望在新 Activity 中打开登录页
    // 但如果你在当前页内实现了 WebView 控件,则在此回调中使用 tag 注册 WebView 即可
    override fun openNewPage(tag: String): Boolean {}
})

# CUSTOM_TOKEN

val result = IFlyHome.setCustomToken(customToken)

登录失败的参数 type 含义可查阅登录错误状态码。如果没有根据你选用的 loginWay 调用对应的登录方式,则接口会返回 RESULT_ILLEGAL_PARAMS

# 注销

使用以下方式可以注销 SDK 登录状态

val result = IFlyHome.logout(object : IFlyHomeLogoutCallback {
    override fun onLogoutSuccess() {}

    override fun onLogoutFailed(t: Throwable?) {}
})

# 绑定模块

设备请求认证可以生成一个授权 URL,你需要将这个 URL 调用以下方法打开认证

val result = IFlyHome.openAuthorizePage(webView, url, object: AuthResultCallback {
    override fun onSuccess() {
        // 授权成功
    }

    override fun onFailed(message: String) {
        // 授权失败
    }
})

回调参数可不传,若用户打开授权页面后时直接关闭网页,则不会调用任何回调。

注意:允许授权后,视网络情况而定,设备也许不能立即通过认证并连接到 IVS,此时关闭授权成功页面可能需要过一段时间才能从用户绑定设备列表接口获取到该设备

# 接口模块

SDK 提供了以下可供调用的接口,所有的接口都是异步请求,在回调中返回响应体。

接口的响应内容格式各有异同,但如果请求出错,都会以下述格式返回错误信息,你可以用以下信息用于做相关提示。

{
    "code": "code", // 错误码
    "message": "message内容" // 错误信息描述
}

由于接口数量过多,建议在顶部的目录概述中先找到所需的接口名称,点击跳转到指定位置。

# 获取用户已绑定设备列表

val result = IFlyHome.getUserDevices(callback)

参数说明

callback 为网络请求的回调接口,包含以下回调,后不赘述

interface ResponseCallback {
    fun onResponse(response: Response<String>)

    fun onFailure(call: Call<String>, t: Throwable)
}

响应内容

{
    "user_devices": [
        {
            "name": "{Device name}",
            "image": "{Device image url}",
            "device_id": "{Device id}",
            "client_id": "{Client id}",
            "brand": {
                "name": "{Brand name}",
                "logo": "{Brand logo url}",
                "id": 1
            }
        }
    ]
}

# 取消绑定设备

val result = IFlyHome.deleteUserDevice(deviceId, callback)

响应内容

{
    "device_id": "..."
}

device_id 若与传入参数一致则表示取消绑定成功。传参不合法则可能返回 400 状态码。

# 获取音频内容列表

val result = getMusicGroups(deviceId, callback)

参数说明

字段 类型 必须出现 说明
deviceId String 设备 ID

响应内容

{
    "groups": [
        {
            "type": "charts",
            "has_more": true, // 表示此 group 是否除了 items 之外还有更多榜单/歌单
            "name": "热门榜单",
            "section": "1",
            "items": [
                {
                    "name": "咪咕热销榜",
                    "image": "https://ss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=3526416775,3196812106&fm=58&bpow=512&bpoh=512",
                    "id": "10002",
                    "description": "热销音乐",
                    "source_type": "migu"

                }, ...
            ],
            "descriptions": [
                "嘿,小飞,最近有什么新歌", ...
            ]
        }, ...
    ],
    "banners": [
        {
            "image": "http://p1.music.126.net/HJNFDYEAfvQlrQR_9q33zw==/109951163315205415.jpg"
        }, ...
    ]
}

# 获取内容分组全部列表

val requestResult = IFlyHome.getGroupSection(sectionId, callback)

参数说明

字段 类型 必须出现 说明
sectionId String 分组 ID,在此接口获取的 groupsitemsectionId

响应内容

{
    "section_id": "2",
    "name": "热门节目",
    "items": [
        {
            "name": "抗癌:第一时间的抉择",
            "description": null,
            "image": "https://ttfm2018pub-oss-cdn.tingtingfm.com/cover/2018/0911/4d/db/4ddbe486d0dd4d14ab58430059a1474a.jpg?x-oss-process=image/resize,h_800,w_800",
            "id": "29"
        },
        {
            "name": "男人战争",
            "description": null,
            "image": "https://ttfm2018pub-oss-cdn.tingtingfm.com/cover/2018/0919/67/c5/67c56824b1708d1190dc7b6bf913c071.jpg?x-oss-process=image/resize,h_800,w_800",
            "id": "46"
        },
        {
            "name": "思维风暴:让你的大脑动起来",
            "description": null,
            "image": "https://bookpic.lrts.me/8e8c8b793de141159c3e575c190ec60b_180x254.jpg",
            "id": "50"
        },
        ...
    ],
    "descriptions": [
        "嘿,小飞,最近有什么歌"
    ]
}

# 获取指定歌单/榜单下的歌曲列表

val result = IFlyHome.getSongs(groupId, page, limit, callback)

参数说明

字段 类型 必须出现 说明
groupId String 歌单/榜单 ID
page Int 页码
limit Int 单页条目数量

响应内容

{
    "songs": [
        {
            "name": "我们(电影《后来的我们》主题曲)",
            "id": "c229e159dd60591d776ff08afbb66a83",
            "artist": "陈奕迅",
            "liked": true,
            "available": true
        }, ...
    ],
    "name": "咪咕影视榜",
    "image": "http://p1.music.126.net/HJNFDYEAfvQlrQR_9q33zw==/109951163315205415.jpg"
}

# 搜索音乐

val result = IFlyHome.searchMusic(deviceId, keyword, page, limit, callback)

参数说明

字段 类型 必须出现 说明
deviceId String 设备 ID
keyword String 搜索关键字

响应内容

{
    "total": 3,
    "page": 1,
    "limit": 10,
    "results": [
        {
            "name": "我们(电影《后来的我们》主题曲)",
            "id": "c229e159dd60591d776ff08afbb66a83",
            "artist": "陈奕迅"
        }, ...
    ]
}

# 获取当前支持信源(已过时)

val result = IFlyHome.getCollectionSource(callback)

响应内容

{
    "sources": [
        {
            "name": "酷狗音乐",
            "icon": "https://..",
            "source_type": "kugou",
            "user_collections_count": 10
        },
        {
            "name": "咪咕音乐",
            "icon": "https://..",
            "source_type": "migu",
            "user_collections_count": 20
        }
    ]
}

# 获取当前支持收藏的标签列表

val result = IFlyHome.getCollectionTags(callback)

响应内容

{
    "tags": [
        {
            "name": "音乐",
            "id": "0"
        },
        {
            "name": "儿童",
            "id": "1"
        }
    ]
}

# 获取指定信源下,用户已收藏音乐列表(已过时)

val result = IFlyHome.getCollectionFromSource(deviceId, sourceType, callback)

参数说明

字段 类型 必须出现 说明
deviceId String 设备 ID,若不传,则所有收藏中的歌曲都无法播放,且字段中 availablefalseerror_reason 中会携带错误原因
sourceType String 信源类型,来自该接口的响应

响应内容

{
    "total": 7,
    "collections": [
        {
            "name": "我们的爱没有错(《泡沫之夏》电视剧主题曲)",
            "id": "02a24b7e00d53c879fd10a783c16f7ca",
            "artist": "胡夏",
            "available": true,
            "error_reason": ""
        }, ...
    ]
}

# 获取指定标签下,用户已收藏音乐列表

val result = IFlyHome.getSongCollection(deviceId, tagId, callback)

参数说明

字段 类型 必须出现 说明
deviceId String 设备 ID,若不传,则所有收藏中的歌曲都无法播放,且字段中 availablefalseerror_reason 中会携带错误原因
tagId String 信源类型,来自该接口的响应

响应内容

{
    "total": 7,
    "collections": [
        {
            "name": "我们的爱没有错(《泡沫之夏》电视剧主题曲)",
            "id": "02a24b7e00d53c879fd10a783c16f7ca",
            "artist": "胡夏",
            "available": true,
            "error_reason": ""
        }, ...
    ]
}

# 收藏音乐(已过时)

val result = IFlyHome.likeMusic(mediaId, sourceType, callback)

参数说明

字段 类型 必须出现 说明
mediaId String 音乐 ID
sourceType String 信源类型,来自该接口的响应

响应内容

{
    "message": "收藏成功",
    "code": "0000"
}

若收藏出错,message 字段会有相关错误提示。后同。

# 收藏音乐

val collectionBody = CollectionBody(
    likeType,
    mediaId,
    sourceType,
    business,
    tagId,
    albumId,
    mediaIds
)
val result = IFlyHome.musicCollected(collectionBody, responseCallback())

参数说明

字段 类型 必须出现 说明
likeType String 收藏类型。
1. album 则代表将要收藏专辑
2. single 则代表将要收藏单曲
mediaId String 单曲 id。若 likeType 不为 single 则可以传 null
sourceType String 信源类型,歌曲信息中会携带。若 likeType 不为 single 则可以传 null
business String 业务类型。歌曲信息中会携带
tagId Int 标签类型。歌曲信息中会携带
albumId String 专辑 id。若 likeType 不为 album 则可以传 null
mediaIds ArrayList of String 单曲 id 列表。可用于批量收藏操作,不需要时可传空

响应内容

{
    "message": "收藏成功",
    "code": "0000"
}

若收藏出错,message 字段会有相关错误提示。后同。

# 取消收藏音乐(已过时)

val result = IFlyHome.unlike(mediaId, sourceType, callback)

# 取消收藏音乐

val result = IFlyHome.deleteCollection(collectionBody, callback)

参数说明

collectionBody 中各参数同 收藏音乐 接口中的参数定义。

# 获取设备媒体控制状态

val result = IFlyHome.getMusicControlState(deviceId, callback)

响应内容

{
    "speaker": {
        "volume": 50,
        "mute": false
    },
    "music_player": {
        "playing": true
    },
    "music": {
        "srouce": "咪咕音乐",
        "name": "告白气球",
        "image": "http://open.migu.cn:8100/material/pics/album/l/2017/04/28/1611190117569581.jpg?m",
        "id": "236945c65a1493344ae190d2cfe80217",
        "liked": false,
        "artist": "周杰伦",
        "source_icon": "https://...",
        "source_description": "由 咪咕音乐 提供"
    }
}

# 控制设备播放音乐

val result = IFlyHome.musicControlPlay(deviceId, mediaId, sourceType, callback)

# 控制设备播放用户收藏音乐列表

val result = IFlyHome.musicControlPlayCollections(tagId, deviceId, mediaId, sourceType, callback)

参数说明

字段 类型 必须出现 说明
tagId String 标签 ID,声明收藏列表的标签类型
mediaId String 音乐 ID,表示从列表中的该音乐开始播放,若列表中找不到该 mediaId 或传 null 则默认第一首开始播放

# 获取专辑详情

歌曲信息中包含 albumId 则可以通过此结果获取完整专辑

val result = IFlyHome.getAlbum(albumId, deviceId, sourceType, bussiness, 1, 10 callback)

参数说明

字段 类型 必须出现 说明
albumId String 专辑 ID
sourceType String 信源类型
bussniness String 业务类型
page Int 页码
pageLimit Int 每页数据量

以上数据在各类包含歌曲信息的接口中都已包含,从歌曲信息中取出参数传入即可。

# 播放专辑

播放专辑里的歌曲

val result = IFlyHome.playAlbum(deviceId, albumId, mediaId, sourceType, business, callback)

参数说明

字段 类型 必须出现 说明
deviceId String 设备 ID
albumId String 专辑 ID
mediaId String 歌曲 ID
sourceType String 信源类型
bussniness String 业务类型

# 获取播放记录标签

val result = IFlyHome.getRecordTags(callback)

# 获取最近播放记录列表

val result = IFlyHome.loadPlaybackRecord(deviceId, tagId, callback)

参数说明

字段 类型 必须出现 说明
tagId Int 标签 ID

# 删除播放记录

val result = IFlyHome.deleteRecord(ids, callback)

参数说明

字段 类型 必须出现 说明
ids IntArray 标签 ID

# 播放最近播放记录的单曲

val result = IFlyHome.playRecord(deviceId, tagId, mediaId, sourceType, callback)

参数说明

字段 类型 必须出现 说明
tagId Int 标签 ID
mediaId String 单曲 ID
sourceType String 信源类型

# 获取最近播放歌曲列表

val result = IFlyHome.getPlayList(deviceId, callback)

# 控制设备播放榜单/歌单

val result = IFlyHome.musicControlPlayGroup(deviceId, mediaId, groupId, callback)

参数说明

字段 类型 必须出现 说明
mediaId String 音乐 ID,表示从列表中的该音乐开始播放,若列表中找不到该 mediaId 或传 null 则默认第一首开始播放

返回说明

从 HTTP 状态码可以判断本次请求是否成功,若状态码为 200 则代表本次请求成功,并可能返回以下内容,代表设备当前的播放器状态

{
    "speaker": {
        "volume": 50,
        "mute": false
    },
    "music_player": {
        "playing": true
    },
    "media": {
    	"source_type": "migu",
        "srouce": "咪咕音乐",
        "name": "告白气球",
        "image": "http://open.migu.cn:8100/material/pics/album/l/2017/04/28/1611190117569581.jpg?m",
        "id": "236945c65a1493344ae190d2cfe80217",
        "liked": false,
        "artist": "周杰伦",
        "source_icon": "https://...",
        "source_description": "由 咪咕音乐 提供",
        "support_voice_transform": false
    }
}

若调用播放失败,则 HTTP 状态码可能为 400 到 500 之间,返回的数据具有固定格式,形如

{
    "code": "3001",
    "message": "请先为test开通畅听权限",
    "image": "https://cdn.xxxx.xxx",
    "redirect_url": "https://xxxx/xxx",
    "button_text": "前往开通"
}

在这段示例中,我们推荐接入 SDK 的 App,展示一个带图文及操作的对话框,分别根据返回字段中的值来做用户提示,点击对话框的操作则跳转到指定的 url

其中 message 字段代表用户提示文案,button_text 代表提示操作文案,image 一般为设备图片,redirect_url 为前往配置的 url ,使用上文提及的注册过的 WebView 加载即可。

# 控制设备停止播放

val result = IFlyHome.musicControlStop(deviceId, callback)

# 控制设备继续播放

val result = IFlyHome.musicControlResume(deviceId, callback)

# 控制设备播放上一首

val result = IFlyHome.musicControlPrevious(deviceId, callback)

# 控制设备播放下一首

val result = IFlyHome.musicControlNext(deviceId, callback)

# 控制设备音量

val result = IFlyHome.musicControlSetVolume(deviceId, volume, callback)

参数说明

字段 类型 必须出现 说明
volume Int 音量值 0~100

# 获取设备详情信息

val result = IFlyHome.getDeviceDetail(deviceId, callback)

# 获取当前登录用户资料

val result = IFlyHome.getUserInfo(callback)

# 获取当前授权 Token

若未登录时调用此接口,则会返回 null

val token = IFlyHome.getToken()

响应说明

名称 类型 说明
alias string 设备别名,没有设置时为null
sleep string 定时休眠配置,关闭时为null
sleep_config.enable boolean 定时休眠配置,是否开启
sleep_config.start boolean 定时休眠开始时间
sleep_config.end boolean 定时休眠结束时间
bluetooth_enable boolean 蓝牙是否开启
speaker string 发音人名称
wakeword string 唤醒词
new_version boolean 是否有新版本
infrared object 设备红外控制参数,字段不存在时红外配置
infrared.client_id object 红外控制设备client_id
infrared.logo object 红外控制设备logo
reboot boolean 设备重启,默认为true
reset_network boolean 重新配网,默认为true
restore_factory boolean 恢复出厂默认为true
music_access object 音乐使用权限
music_access.name string 显示名称
music_access.value string 显示值
music_access.redirect_url string 跳转地址
status string 设备在离线状态
children_mode boolean 儿童模式是否开启

# 重置设备网络

val result = IFlyHome.resetNetwork(deviceId, callback)

# 重启设备

val result = IFlyHome.rebootDevice(deviceId, callback)

# 设备恢复出厂

val result = IFlyHome.restoreFactory(deviceId, callback)

# 设置设备别名

val result = IFlyHome.updateDeviceAlias(deviceId, alias, callback)

alias 的长度限制为 20.

# 更新设备信息

val result = IFlyHome.updateDeviceInfo(deviceId, alias, childrenMode, continousMode, zone, callback)

参数说明

字段 类型 必须出现 说明
deviceId String 设备 ID,表示要设置模式的设备
alias String 别名,若不更改则传 null
childrenMode Boolean 是否进入儿童模式,若不更改则传 null
continousMode Boolean 是否开启持续交互,若不更改则传 null
zone String 设备位置,若不更改则传 null

# 为设备添加红外遥控

val result = IFlyHome.infraredDevicesAdd(deviceId, extends, callback)

参数说明

字段 类型 必须出现 说明
deviceId String 支持红外遥控的设备 id
extends JSONObject 红外码相关数据(设备厂商定义规则)

# 红外遥控设备发送红外码

val result = IFlyHome.infraredDevicesAccept(deviceId, extends, callback)

调用接口后,推送给设备一个发送红外码的指令。参数定义参考为设备添加红外遥控的参数说明

# 获取当前账号可绑定的设备信息

val result = IFlyHome.getClientInfos(callback)

相应说明

返回数据为列表,列表中每项数据含义如下表

字段 类型 必须出现 说明
client_image String 设备图片 url
client_name String 设备名称
client_id String 设备 clientId

# 获取指定 clientId 设备的设备信息

val result = IFlyHome.getClientInfo(clientId, callback)

# 获取 deviceCode

用于 BLE 等无需用户二次确认的授权方式

val result = IFlyHome.getDeviceCode(clientId, deviceId, callback)

# 检查 deviceCode 的授权状态

val result = IFlyHome.getDeviceConfirm(deviceCode, callback)

# 设置设备是否开启儿童模式

val result = IFlyHome.updateDeviceInfo(deviceId, alias, true, callback)

参数说明

字段 类型 必须出现 说明
deviceId String 要更改儿童模式的设备 id
alias String 设备别名
isChildrenMode Boolean 是否开启儿童模式

# 获取当前用户唯一标识

val result = IFlyHome.getUserIdentity(callback)

返回说明

返回结果中仅包含唯一字段, UUID 格式的用户标识

# 获取内容账号列表

val result = IFlyHome.getWebAccounts(callback)

返回说明

{
    "accounts": [
        {
            "name": "酷狗音乐",
            "bind_type": "kugou",
            // ...
        }
    ]
}

返回数据中包含可供查看的若干第三方账号列表数据,将 bind_type 传入 打开指定内容账号 可以在 WebView 中查看该账号的具体信息展示。

# 获取训练计划列表

val result = IFlyHome.getTrainingPlanList(callback)

# 添加训练计划

val result = IFlyHome.addTrainingPlan(token, body, callback)

参数说明

字段 类型 必须出现 说明
token String 训练计划唯一 token,token不为空时更新数据,token为空时添加数据
body String 添加训练计划请求参数

body说明

{
    "execType": "voice",
    "execData": {
          "wakeUpWord": [
            "今天好吗"
        ]
    },
    "operations": [
        {
            "date": "今天",
            "city": "广东省|广州市",
            "name": "播报天气",
            "alias": "当前唤醒设备",
            "clientDeviceId": "-1",
            "text": "广东省广州市今天的天气",
            "type": "weather"
        }
    ]
}

body 参数说明

字段 类型 必须出现 说明
execType String 执行类型,有两种类型,语音:voice, 时间: alarm
wakeUpWord Array 唤醒词

各个操作类型 operations 参数如下

1.语音回复

{
  "name": "语音回复",
  "type": "voice_reply",
  "reply": [ //语音回复语料
    "我也不知道啊",
    "不知道该说什么"
  ],
  "alias": "当前唤醒设备",
  "clientDeviceId": "-1"
}
参数名 类型 说明
clientDeviceId string 设备 device id, -1 表示当前唤醒设备,下面用法相同

2.闹钟响铃

{
  "name": "闹钟响铃",
  "type": "alarm",
  "tips": "就是想提醒而已", //语音提示内容
  "alias": "魔飞-pro650",
  "clientDeviceId": "c0bdaffc-3b37-47e3-9d64-03fa620b4393.9c417cb54cd8"
}

3.播报天气

{
  "name": "播报天气",
  "type": "weather",
  "date": "今天",
  "city": "广东省|广州市",
  "text": "广东省广州市今天的天气",
  "alias": "当前唤醒设备",
  "clientDeviceId": "-1"
}
参数名 类型 说明
date string 日期:
今天
明天
后天
city string 城市
text string 显示文案
  1. 播报新闻
 {
   "name": "播报新闻",
   "type": "news",
   "newsType": "热点",
   "text": "播报热点新闻",
   "alias": "当前唤醒设备",
   "clientDeviceId": "-1"
 }
参数名 类型 说明
newsType string 新闻类型
text string 显示文案
  1. 播放音乐
{
  "name": "播放音乐",
  "type": "music",
  "hobby": "场景|摇滚",
  "text": "播放摇滚的歌",
  "alias": "当前唤醒设备",
  "clientDeviceId": "-1"
}
参数名 类型 说明
hobby string 喜好风格
text string 显示文案

execType 为 alarm,execData 传参如下

  1. 每天执行
"execData": {
  "repeat_type": "DAILY",
  "time": "23:15"
}

2.在某个时间点执行

{
  "execData": {
        "repeat_type": "NONE",
        "selectDate": "2021年01月01日",
        "time": 1609514820000,
        "timestamp": 1609514820000
   }
}

3.每月第几日执行

{
    "execData": {
        "repeat_date_number": 24,
        "repeat_type": "MONTHLY",
        "time": "18:27"
    }
}

4.每年的某一日执行

{
    "execData": {
        "repeat_date": "05-24",
        "repeat_type": "YEARLY",
        "time": "21:31"
    }
}

5.法定工作日执行

{
    "execData": {
        "execType": "alarm",
        "repeat_type": "WORKDAY",
        "time": "18:37"
    }
}

6.法定节假日执行

{
    "execData": {
        "repeat_type": "RESTDAY",
        "time": "23:37"
    }
}

7.周末执行

{
    "execData": {
        "repeat_type": "WEEKEND",
        "time": "23:40"
    }
}

8.周一至周五执行

{
    "execData": {
        "repeat_type": "WEEKDAY",
        "time": "23:55"
    }
}

# 删除训练计划

val result = IFlyHome.deletePlan(tokens, callback)

参数说明

字段 类型 必须出现 说明
tokens Array 训练计划 token 数组

# 修改训练计划

val result = IFlyHome.addTrainingPlan(token, body, callback)

参数说明

字段 类型 必须出现 说明
token String token不为空时更新数据,token为空时添加数据
body String 添加训练计划请求参数

# 检查唤醒语料是否合法

val result = IFlyHome.checkPlanValid(text, callback)

# 获取训练计划详细信息

val result = IFlyHome.getPlanDetail(token, callback)

# 获取训练计划操作类型列表

val result = IFlyHome.getPlanActionTypes(execType, callback)

返回结果:

[
    {
        "value": "voice_reply",
        "text": "{reply}",
        "name": "语音回复"
    },
    {
        "value": "weather",
        "text": "{city}{date}的天气",
        "name": "播报天气"
    },
    {
        "value": "news",
        "text": "播报{newsType}新闻",
        "name": "播报新闻"
    },
    {
        "value": "music",
        "text": "播放{hobby}的歌",
        "name": "播放音乐"
    },
    {
        "value": "iot",
        "text": "{action_text}",
        "name": "智能家居控制"
    }
]

参数说明

字段 类型 必须出现 说明
execType String 执行类型,只有两种情况,语音:voice, 时间:alarm

# 获取训练计划操作类型详情

val result = IFlyHome.getPlanActionType(execType, actionType, callback)

例如 actionType 为 iot(智能家居返回结果):

[
    {
        "moreItem": true,
        "name": "选择设备",
        "column": "devices",
        "type": "select",
        "items": [
            {
                "iotId": 1,
                "deviceTypeId": 5,
                "zone": "默认房间",
                "id": 54,
                "deviceName": "三楼热水器",
                "brand": "Orvibo",
                "friendlyName": "三楼热水器"
            }
        ]
    }
]

参数说明

字段 类型 必须出现 说明
execType String 执行类型,只有两种情况,语音:voice, 时间:alarm
actionType String 操作类型,获取训练计划操作类型列表 可获取相关的 actionType

# 获取训练计划 moreItem 数据

val result = IFlyHome.getPlanMoreItem(execType, actionType, iotId, body, callback)

参数说明

字段 类型 必须出现 说明
execType String 执行类型,只有两种情况,语音:voice, 时间:alarm
actionType String 操作类型,获取训练计划操作类型列表 可获取相关的 actionType
iotId String 可在 获取训练计划操作类型详情 接口获取
body String 请求body,可在 获取训练计划操作类型详情 接口获取

body的参数如下

{
    "iotId": 1,
    "deviceTypeId": 5,
    "zone": "默认房间",
    "id": 54,
    "deviceName": "三楼热水器",
    "brand": "Orvibo",
    "friendlyName": "三楼热水器"
}

# 订阅状态变更事件

SDK 中提供了基于 WebSocket 的推送服务,可用于 订阅设备在线、离线状态的通知 以及 设备媒体播放状态变更的通知

Demo 中的参考实现在 PushDemoActivity 中。

# 准备工作

你需要实现一个继承 OnPushService 的服务,并注册到 AndroidManifest.xml 中。后续工作都是通过调用这个 Service 来实现。

启动服务后,通过 bindService 获取到这个 Service 的实例。

# 订阅设备在线/离线

订阅设备在线/离线状态变更首先需要知道用户的 userId ,订阅成功后,用户所绑定的设备列表中在/离线状态变更后 Service 就会收到相关的通知。

该字段可以通过 获取当前登录用户资料 获取。

val result = service?.subscribeUserDeviceState(userId)

# 订阅设备媒体播放状态变更

设备媒体播放状态变更,指代的是与媒体播放相关状态有关的一切状态,包括设备正在播放的媒体内容变化、设备媒体音量发生变化。

订阅设备媒体播放状态变更首先需要知道设备的 deviceId ,该字段可以通过 获取用户已绑定设备列表 获取。

val result = service?.subscribeDeviceMediaState(deviceId)

# 接收通知

为 Service 绑定回调

service.addSubscribeCallback(subscribeCallback)

回调中包含的函数定义如下,推送回调的返回值也在函数注释中做出对应的解释说明。你仅需实现你所需要的部分函数即可

    open class SubscribeCallback {
        /**
         * 监听用户设备在离线状态,订阅成功
         * @param userId 用户 id
         */
        open fun onUserDeviceStateSubscribed(userId: String) {}

        /**
         * 监听用户设备在离线状态,取消订阅成功
         * @param userId 用户 id
         */
        open fun onUserDeviceStateUnsubscribed(userId: String) {}

        /**
         * 监听设备媒体播放状态,订阅成功
         * @param deviceId 设备 id
         */
        open fun onDeviceMediaStateSubscribed(deviceId: String) {}

        /**
         * 监听设备媒体播放状态,取消订阅成功
         * @param deviceId 设备 id
         */
        open fun onDeviceMediaStateUnsubscribed(deviceId: String) {}

        /**
         * 设备媒体播放状态更新
         *
         * message 内容包括
         * ```javascript
         * {
         *  "data":{
         *      "music_player":{
         *          "playing":true // means if this device is playing any audio
         *      },
         *      "music":{
         *          "image":"SOME_URL", // might be url of a album cover or artist picture, might be null or empty
         *          "source_icon":"SOME_URL", // icon of source type, might be null or empty
         *          "can_like":false, // means if this media can be like
         *          "artist":"MediaArtist", // artist's name, might be null or empty
         *          "name":"MediaName", // media name
         *          "tag_id":3, // tag for liking this media, if it is likable
         *          "source_type":"MediaSourceType", // source type of kugou, ximalaya or others, might be null or empty
         *          "source":"MediaSourceTypeName", // friendly name of source type, might be null or empty
         *          "id":"61608185", // media id
         *          "source_description":"" // source description
         *      },
         *      "device_id":"ceb62135-38a7-42c3-b721-842f3ed596a8.7949dfab962fa371",
         *      "speaker":{
         *          "volume":5, // volume of the device, max is 100
         *          "mute":false // means if the device is muted
         *      }
         *  },
         *  "type":"music_state",
         *  "timestamp":1606298683772 // means the state timestamp
         *  }
         * ```
         *
         * 注意:
         *
         * 由于长连接异步接收,不能保证所有推送都按顺序回调。
         *
         * 所以,同一设备如果收到了播放状态,需要暂存与设备对应的 `timestamp`。
         *
         * 若新接收的状态 `timestamp` 比已使用的 `timestamp` 小,则此状态应被丢弃
         *
         * @param deviceId 发生状态变更的设备 id
         * @param message 设备媒体状态变更信息
         */
        open fun onMediaStateMessage(deviceId: String, message: String) {}

        /**
         * 设备在离线状态更新
         *
         * ```
         *{
         *  "data":{
         *      "device_id":"ceb62135-38a7-42c3-b721-842f3ed596a8.7949dfab962fa371",
         *      "state":"offline",
         *      "client_id":"ceb62135-38a7-42c3-b721-842f3ed596a8"
         *  },
         *  "type":"device_state",
         *  "timestamp":1606300318946
         *}
         * ```
         *
         * 根据 `data.state` 状态判断,`online` 代表在线,`offline` 代表离线
         *
         * @param userId 发生状态变更的设备所属用户 id
         * @param message 设备在离线状态变更信息
         */
        open fun onDeviceStateMessage(userId: String, message: String) {}

        /**
         * 连接建立失败回调
         * @param type 可能为 UserDeviceState 代表设备在离线状态,DeviceMediaState 代表设备媒体播放状态
         * @param data 订阅时传递的参数,若是在离线状态则是 userId,若是媒体播放状态则是 deviceId
         * @param t 连接失败的异常信息
         */
        open fun onConnectFailed(type: SubscribeType, data: String, t: Throwable) {}
    }

另外地,设备在线状态可通过 获取设备详情信息 获取,设备媒体播放状态可以通过 获取设备媒体控制状态 获取,用于用户在首次进入某些页面的状态拉取。

# 异常情况处理

# 订阅失败处理

前文提到,两种订阅方式都会返回对应的 result处理 result 是非常重要的措施。根据 result 不同,需要根据你的需要作出对应的业务处理。

val result = service?.subscribeUserDeviceState(userId)

result 值及含义说明

取值 含义
IFlyHome.RESULT_OK 调用成功,但不代表已经订阅成功,订阅成功以 onUserDeviceStateSubscribedonDeviceMediaStateSubscribed 为准
IFlyHome.RESULT_UN_LOGIN 未登录
IFlyHome.RESULT_UNINITIALIZED SDK 未初始化
IFlyHome.RESULT_ILLEGAL_PARAMS 参数错误,表示要订阅的 userIddeviceId 已经在订阅列表中。如要重新订阅需要先调用getSubscribedDeviceStateUsers()getSubscribedMediaStateDevices() 获取已订阅的 userIddeviceId 并取消订阅接口

# 异常回调说明

onUserDeviceStateUnsubscribedonDeviceMediaStateUnsubscribed 是处理连接断开情况的重要回调。当长连接建立之后,一个可用的长连接被断开之后,这两个回调就会被调用。如果需要进行重连,建议可以在这个回调中进行。

onConnectFailed 回调则代表长连接尚未建立就发生了错误,回调参数中的 t: Throwable 用于判断错误类型。这里的错误基本由网络框架抛出,例如当网络不可用时,这里将会抛出 java.net.UnknowHostException ,你可以根据你的业务需要,做出对应的用户提示。

# 配网模块(已开源)

# 扫码配网模块

在 SDK Demo 中,使用 zxing 来执行扫码的操作,然后对扫码得到的数据进行访问。

扫码得到的数据是一个进行授权的 URL,通过 SDK 的接口来访问这个 URL,可以打开授权页面。

IFlyHome.openAuthorizaPage(webView, url) // 此处的 WebView 应当是调用过 IFlyHome.register 注册过的 WebView

# 声波配网模块

SDK Demo 中提供了一个通用的、开源的声波播放器。下面介绍如果使用这部分开源代码。

# 初始化

val player = SoundWavePlayer(context)

# 播放

player.prepare(true, ssid, password, object : SoundWavePlayer.PrepareListener {
    override fun onPrepared(player: SoundWavePlayer) {
        player.start()
    }
})

参数说明

字段 类型 必须出现 说明
reAuth Boolean 是否需要重新授权
ssid String WiFi 名称
password String WiFi 密码,为空时视为无密码 WiFi 网络
PrepareListener Function 播放器设置回调

注意:需要在 PrepareListener 回调时再调用播放器的 play

# 停止

player.stop()

# 蓝牙配网模块

蓝牙配网设备端的逻辑可参考此处描述声明

当设备端进入配网模式后,App 可以搜索到指定的标识设备的 BLE 广播。

注意

在 Android 上搜索 BLE 设备,需要获取相关的 位置权限。这部分逻辑可以参考 Demo 中的源码,但我们建议你实现自己的权限申请。

# 搜索 BLE 设备

参照 BleScanActivity 中,搜索 BLE 设备的相关逻辑。

// 绑定 BLE 服务后,开始扫描
bleService.startScan()

// Demo 包名内的 BleDeviceStorage 会存储扫描到的设备及相关的设备信息
BleDeviceStorage.addCallback(callback)

// callback 中要实现
override fun onDeviceScanned(deviceInfos: HashSet<BleDeviceStorage.DeviceInfo>) {
    // 据此可以得到当前扫描到的所有设备,并做相关的显示
    deviceInfos.map{ deviceInfo -> 
        val device: BluetoothDevice = deviceInfo.device // 搜索到的 Ble 设备
        val clientId: String = deviceInfo.clientId      // 该 Ble 设备的 client id

        // 获取可用于 UI 显示的设备信息
        BleDeviceStorage.getClientInfo(clientId) { clientInfo ->
            val clientImage = clientInfo.clientImage    // 设备图片 url
            val clientName = clientInfo.clientName      // 设备名称
            val clientId = clientInfo.clientId          // 设备的 client id
        }
    }
}

# 连接到 BLE 设备

BLE 对设备操作的相关代码位于 HandleBleDeviceActivity

  1. 请求连接
val connect = Intent(this, BleService::class.java)
connect.action = BleService.ACTION_CONNECT
connect.putExtra(BleService.EXTRA_DEVICE, device)
startService(connect)
  1. 连接结果回调
// 注册以下广播则可以获取相关的状态回调
val intentFilter = IntentFilter()
intentFilter.addAction(BleService.ACTION_DEVICE_CONNECTED)
intentFilter.addAction(BleService.ACTION_DEVICE_CONNECT_FAILED)
intentFilter.addAction(BleService.ACTION_DEVICE_DISCONNECTED)
registerReceiver(bleReceiver, intentFilter)

// 特别地,连接失败时,可以获取相关的错误原因,此部分错误原因为 Ble 原生数据,与 iFLYOS 无关
...
val result = intent.getIntExtra(BleService.EXTRA_REASON, -1)
// result 所有可能的常量,在 GattError 类中有定义
...

# 发送相关数据

连接设备成功后,首先需要从设备读取设备的唯一标识

val requestDeviceId = Intent(context, BleService::class.java)
requestDeviceId.action = BleService.ACTION_REQUEST_DEVICE_ID
startService(requestDeviceId)

// 广播接收器中接收 BleService.ACTION_DEVICE_ID_RECEIVED
...
val deviceId = intent.getStringExtra(BleService.EXTRA_DEVICE_ID)
...

根据 deviceId 申请供设备授权使用的 deviceCode

IFlyHome.getDeviceCode(clientId, deviceId, object : ResponseCallback {
    ...
    override fun onResponse(response: Response<String>) {
        if (response.isSuccessful) {
            val json = JsonParser().parse(response.body()).asJsonObject
            val deviceCode = json.get("device_code").asString
        }
    }
    ...
})

发送配网的相关数据给设备

val setup = Intent(context, BleService::class.java)
setup.action = BleService.ACTION_SETUP
setup.putExtra(BleService.EXTRA_DEVICE_CODE, deviceCode)    // 上文中申请到的 deviceCode
setup.putExtra(BleService.EXTRA_SSID, "SSID")               // WiFi 名称
setup.putExtra(BleService.EXTRA_PASSWORD, "PASSWORD")       // WiFi 密码,传空则代表无密码的 WiFi,但部分设备可能不支持不安全的无线网络
startService(setup)

# AP 配网模块

SDK Demo 中开源了 AP 配网的客户端实现,本章仅介绍源码中的部分代码使用。连接传输协议详见 AP 配网

使用 Demo 进行配网时,请先点击 Demo 首页的初始化并进行登录,因为涉及授权码部分需要用户验证信息,所以要求登录后才能使用。

AP 配网相关关键代码主要在 ApConfigNetActivityApConfigNetService ,前者负责界面 UI ,后者负责主要通讯逻辑。

# 如何使用

  1. 进入 AP 配网页面后,首先点击授权码右侧的刷新按钮,在弹窗中填入 client_id 用于获取授权码,授权码用于发送给设备让设备可以获取到 token 。
  2. 获取到授权码之后,连接到设备发出的热点,并在 Demo 上方刷新网关地址查看是否可以正确获取网关。
  3. 点击连接按钮,客户端将与设备端建立 Socket 连接。
  4. 连接成功后,点击发送时间戳。此时设备端应发送 client_id 给客户端用于校验设备是否正确。
  5. 校验 client_id 无误后,点击发送配置,设备将用配置中的 ssid 相关配置开始联网,联网成功后则会开始使用授权码进行授权绑定。
  6. 将手机连接回正常可用的网络后,点击授权码刷新按钮右侧的检查图标,可以检查授权码是否已正确授权。

# 迁移到你的项目

ApConfigNetService 可以直接不做修改直接迁移到你的项目中,该服务提供了三个主要的 Action

# 1. 连接到设备端 - ApConfigNetService.ACTION_CONNECT

使用此 Action 启动服务后,服务会尝试连接到当前网关地址,8080 端口的 Socket 服务。

若连接成功则回调 onOpen 函数,若失败则会回调 onClose() 函数。相关监听可以通过 bindService 绑定服务后为该服务 addListener

# 2. 断开连接 - ApConfigNetService.ACTION_DISCONNECT

使用此 Action 启动服务后,若已连接到设备端,则会断开当前已连接的 Socket 。

# 3. 发送配置 - ApConfigNetService.ACTION_SEND_MESSAGE

使用此 Action 启动服务后,从 extra 获取到 ApConfigNetService.EXTRA_MESSAGE 携带的字符串,发送到设备。具体应发送什么内容给设备端,请参阅 AP 配网协议

# API 接口调用

AP 配网用到了两个 API 接口,分别是 获取用户授权码检查授权码授权状态

# 获取用户授权码
IFlyHome.getClientAuthCode(clientId, scope, callback)

其中 clientId 是必填参数,scope 代表 clientId 对应部分权限,传 null 亦可。

# 检查授权码授权状态
IFlyHome.checkAuthCodeState(authCode, callback)

# 登录错误状态码

状态码 说明
0 网络错误
1 用户主动拒绝授权

# IFLYOS 提供页面

参数变量 说明
ACCOUNTS 内容账号页面,显示与 IFLYOS 账号绑定的第三方账号
CLOCKS 我的闹钟页面,提供绑定设备上的闹钟查看和设置功能
CONTROLLED_DEVICES 被控设备页面,显示可被操作的被控智能家居设备
SKILLS 语音技能相关页面,查看设备可用的技能及介绍
PERSONAL_SOUNDS 个性音库页面,控制可设置到设备上的发音人音库,实验性功能
WAKEUP_WORDS 指定设备自定义设备唤醒词,需指定 deviceId 参数
TIME_TO_SLEEP 指定设备设置定时休眠,需指定 deviceId 参数
SPEAKER 指定设备设置 TTS 发音人,需指定 deviceId 参数
BLUETOOTH 指定设备设置蓝牙开关,需指定 deviceId 参数
CHECK_UPDATE 指定设备检查更新,需指定 deviceId 参数
DIALOGUE 指定设备的对话页,显示当前设备发生的交互内容,需指定 deviceId 参数
COMMUNICATION_SETTING 指定设备的聊天设置页,显示当前设备人设、语言、音色等参数的页面,需指定 deviceId 参数
MESSAGE 指定设备的留言板页,显示当前设备的留言板浏览页面,需指定 deviceId 参数
DEVICE_ZONE 指定设备的设置设备位置页,显示当前设备设置位置的页面,需指定 deviceId 参数
TRAINING_PLAN 训练计划页

# 状态码

返回结果 常量 含义
0 RESULT_UNINITIALIZED SDK 未初始化
1 RESULT_OK 调用成功
-1 RESULT_NO_WEB_VIEW 无法找到指定的 WebView
-2 RESULT_ILLEGAL_PARAMS 传入参数不合法
-3 RESULT_UN_LOGIN SDK 未登录