# Android SDK

::: error 说明 IVS SDK 已停止迭代,新设备若需要接入,请使用EVS API进行接入。若需要SDK,请联系商务。 :::

介绍

iFLYOS 提供了开源的 Android SDK 用于快速接入,封装了 IVS API 的交互实现。厂商既可以直接使用 Android SDK,也可以在它的基础上做二次开发。

更新日期 更新内容
2018.12.10 更新 2.0 版本,基于 Linux SDK 进行 JNI 封装
2019.05.31 更新 3.0 版本,基于 Linux SDK 进行 IPC 通讯
  1. 支持自定义唤醒词
  2. 增加 overlay 模块,尝试兼容在外部应用中的语音唤醒
  3. 适配爱奇艺语音版(如需接入请联系商务)
2019.07.09 更新 3.0 r2 版本
  1. 更新 custom 相关内容
  2. 增加可自定义授权 scope
2019.07.31 更新 3.0 r4 版本,更新 Linux SDK 内核,新增 Custom 支持部分新技能
2019.08.05 更新 3.0 r5 版本,新增一个简易的基于 iFLYOS 设备控制台自动更新功能的 OTA 模块

# Android SDK 介绍

# 开发环境要求

  • Git
  • Android Studio 3.1.2 或以上
  • 能够正常访问 jCenter、Google Maven 的网络连接
  • 一部 Android 4.4 或以上的开发设备(或以 Android 平板代替)

# 快速上手

下载源码:

git clone https://github.com/iFLYOS-OPEN/SDK-Android

使用 Android Studio 打开同步下来的 SDK-Android 工程。首次打开需要等待 Gradle 联网下载一些依赖库,可能需要较长时间,需要保持网络畅通。

连上开发设备,运行 app 模块。这就是一个最基础的 iFLYOS 设备端实现。

# 模块说明

SDK-Android 项目中包含以下几个模块。

# app

包含应用层的主要代码,包含 配网授权调用 iFLYOS SDK 等功能的简单实现。

# template

包含对模板渲染下发的指令的 Fragment 实现封装。

# overlay

(实验性) 用于在应用外唤醒时显示转写界面和 template 模板。使用过程中,可能有未优化好的情况出现,欢迎在 Issue 中提出。

点击下载overlay (opens new window)

# ota

(实验性) 用于在应用启动时后定期的的自动检查更新,目前定义的规则为,启动后开始一次检查,此后每24小时检查一次。在关于页面也可以触发检查更新,触发后定时器重置为下一次的24小时。

# 主程序运行流程介绍

  1. 联网
  2. Demo 中提供了一个简单的连接 WiFi 的界面,可进行网络选择跟密码输入功能。若已联网,该页面也可直接点击下一步。

  3. 授权
  4. iFLYOS 在设备端的启动需要授权,通过使用小飞在线App或接入配套App SDK可对设备进行授权。这里提供了一个有屏配网的开发样例,通过显示二维码的方式对设备进行授权。具体见此处描述

  5. 启动
    1. 应用启动后,应调用 iFLYOSManager.getInstance().init(context, clientId, configPath, iflyosListener, additionalConfigParams) ,初始化 SDK 内部参数。
    2. configPath 表示配置文件的存储路径, iflyosListener 可用于接收 SDK 分发的事件,additionalConfigParams 可以用于传入额外的配置参数。在 SDK 初始化后,可以在 configPath 传入的路径查看到配置文件,配置文件为 json 格式。

      iflyosListener 回调的参数含义参考附录

    3. 授权完成后,通过调用 iFLYOSManager.getInstance().startIFlyOS() 的方式启动 iFLYOS。

# 部分重要配置参数介绍

# 设备 client id

设备 client id 对应你在设备接入控制台创建的设备,将设备中的 client id 换成你创建的 client id ,才能体验到在设备接入控制台中对你的设备的自定义配置。

Demo 中更换 client id 有两种方式:

  1. 修改此处 (opens new window)定义的静态变量,替换为你的 client id

  2. client id 在配置文件中的字段表示为 deviceInfo.clientId。在 genenrateAdditionalParams() (opens new window) 函数中,返回的 json 对象需要包含如下字段

{
  ... // 其他配置
  "deviceInfo": {
      "clientId": "......" // 替换成你自定义的 client id
  },
  ... // 其他配置
}

# 设备唯一标识

通常我们将这个参数称之为 deviceId,在配置文件中的字段表示为 deviceInfo.deviceSerialNumber

在 SDK 的初始化函数中,默认使用设备序列号作为 deviceId,故需要使用 READ_PHONE_STATE 权限。

val serial = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) Build.SERIAL else Build.getSerial()

如果未授予权限,会导致 deviceId 为空,致使授权模块无法正确生成二维码。故在 onResume() (opens new window) 的代码中,尝试更新了 SDK 中的配置参数。

如果你需要在 SDK 初始化之后但仍未启动时 ,更新自定义的参数,参考此处代码调用 updateParams(additionalConfigParams) 即可。若 SDK 启动后(即已调用了 startIFlyOS()),updateParams(additionalConfigParams) 未必能使所有的配置参数生效。

你可以自定义生成 deviceId 的方式。如上文,在 genenrateAdditionalParams() (opens new window) 函数中,返回的 json 对象需要包含如下字段

{
  ... // 其他配置
  "deviceInfo": {
      "deviceSerialNumber": "......" // 替换成你自定义的 device id
  },
  ... // 其他配置
}

注意

如果需要同时自定义 client id 和设备唯一标识,需要合并上述两个字段为

{
  ... // 其他配置
  "deviceInfo": {
      "deviceSerialNumber": "......", // 替换成你自定义的 device id
      "clientId": "......" // 替换成你自定义的 client id
  },
  ... // 其他配置
}

# 录音硬件接入

在启动 iFLYOS 的时候,需要传入一个 SpeechRecognizer 类用于正确处理录音识别的各个操作。

iFLYOSManager.getInstance().startIFlyOS(SpeechRecognizer)

IVS Android SDK 默认通过 SpeechRecognizerHandler (opens new window))来实现录音功能。

默认的录音实现基于 Android AudioRecord API 执行录音操作。(由于现阶段各家厂商为了提升语音识别的体验,会使用定制的录音模块,故未必能直接使用 Android 原生的录音 API,那么只需通过定制此类中的录音写入数据的相关代码实现即可。)

启动录音的操作:

private fun startRecording() {
    ......
    mAudioInput.startRecording()
    ......
    // Read recorded audio samples and pass to engine
    mExecutor.submit(AudioReader()) // Submit the audio reader thread
}

录音线程的操作:

private inner class AudioReader : Runnable {
    ......
    override fun run() {
        var size: Int

        while (isRunning) {
            mAudioInput?.let {
                size = it.read(mBuffer, 0, mBuffer.size)
                if (size >= 320 && isRunning) {
                    write(mBuffer, size, 0) // Write audio samples to engine

                    ......
                }
            }
        }
    }
}

在线程中不断调用 write(byte[] data, long size) 方法,将录音数据持续写入目标文件中。目标文件存储在 ${context.cacheDir.path}/${getFifoFileName()},复写 getFifoFileName 函数可以定义存储的文件名。

默认的录音实现中,我们为你实现了一个通用的唤醒模块,可以通过 「蓝小飞」 方式唤醒。

  • 如果想要取消唤醒功能,在SpeechRecognizerHandler (opens new window)的构造函数中,wakeWordSupported以及wakeWordEnabled参数传入 false 即可。
  • 如果你需要从刚开始启动引擎就开始录音,但不需要我们内置的唤醒功能的话,复写 wakewordDetected (opens new window) 返回 false 即可,表示不处理内置唤醒模块的唤醒事件。

如果你的设备采用了专有的录音方案(如多麦克风降噪),也可以通过复写模块中执行读录音线程相关(AudioReader (opens new window)的代码,就可以很方便地将你的录音方案对接到 SDK 中。

# 主要功能介绍

# 绑定授权

运行 Demo 的第一步需要为 SDK 进行授权。进行授权需要调用 app module 中的授权模块。主要代码位于 com.iflytek.cyber.iot.show.core.impl.AuthProvider (opens new window) 包名下,下面介绍应当如何使用授权功能。

# 1. 启动授权

EngineService (opens new window) 服务会处理 EngineService.ACTION_LOGIN,触发 AuthProvider 开始登陆操作,申请 user_code

# 2. 处理授权 URL

在 Demo 中,将授权 URL 转换为一个二维码图片展示在界面中,通过 iFLYOS 配套 APP SDK 即可打开对应的授权页面。

二维码界面对应的类为 PairFragment (opens new window)

首先,需要注册对应的 Observer,可以通过直接调用 activity.addObserver(...) (opens new window) 来注册,注册后所有的事件会传递到对应的 Observer 中。

然后,在调用登录操作之后,会发送 typeLoggerHandler.CBL_CODE 的事件,并且在其中携带相关的参数。(示例代码:跳转到 GitHub (opens new window))通过 verification_uriuser_code 参数,生成对应的 URL,传入配套 APP SDK 的对应接口即可(Android 接口说明iOS 接口说明)。

# 3. 授权完成回调

授权完成后,可以接收到 typeLoggerHandler.AUTH_LOG 的事件,获得其中的 auth_state 字段为 AuthState.REFRESHED 时,表示授权完成。(参考实现 (opens new window)

# 自定义指令相关

首先,通过以下方式注册一个用于 Custom 相关交互的实例

val customAgent = object : CustomAgent(manager!!) {
    override fun onCustomDirective(directive: String) {
        // 在该回调中可以处理服务端下发的 Custom 指令
        System.out.println("custom: $directive")
    }

}
iFLYOSManager.getInstance().setCustomAgent(customAgent)

CustomAgent 提供以下两个函数可供调用,

// 更新 Context
fun updateContext(context: String)

// 上报 Custom 事件
fun sendCustomEvent(event: String)

TIP

此处的上报 Custom 事件需要你根据协议好的格式传入完整的数据来发送,数据中应包括 context 以及 event,具体字段含义请参考此处描述

# 自定义指令实验性功能

我们为 ShowCore 所使用的 client id 定制了部分功能并不完善的自定义技能,这部分技能特性在 ShowCore 上的表现已经收尾。这些技能在后续开发过程中会持续完善,并改进为公有的能力在 EVS 中实现。

由于这部分技能是给 ShowCore 的 client id 定制的,因此如果你的设备使用了自定义的 client id,并且想要接入这部分技能的话,需要与我们联系协调,我们可以为你开放这部分并不完善的技能。

以下是这些技能的概述。

  1. 屏幕亮度调节、开关显示

在 Custom 指令中,表现为 payload 中,headerNamescreen 开头的相关指令。屏幕亮度调节在 Android 上的实现需要允许修改系统设置权限。开关显示代表设备进入一个比较不打扰用户的显示界面,在全黑的屏幕中仅显示时钟界面。

  1. 关机能力

在 Custom 指令中,表现为 payload 中,headerNamesystem.power_off 的相关指令。ShowCore 中提供一个默认实现,关闭了屏幕显示(并非息屏,请参考上一条中的描述),并且停止了所有的正在进行的音频播放(包括闹钟)。

  1. hdp 直播调用

在 Custom 指令中,表现为 payload 中,headerNamehdp 开头的相关指令。提供了简单的跳转到 HDP 直播应用的部分使用方式。

# 自定义唤醒词 (实验性)

# 生成唤醒词资源

你可以在 iFLYOS 企业平台中生成浅定制唤醒词(生成链接 (opens new window)),在页面中点击 生成唤醒包并下载,会开始下载一个压缩包。下载完成后将压缩包 解压 后得到的文件,将后缀名定义为 .jet,文件名可自定义为你想要的名字。

# 导入唤醒词

将唤醒词资源 **.jet 复制到 assets 目录,在应用启动后将资源复制到你自定义的目录(例如使用 applicationContext.externalCacheDir,这样可避免获取 READ_EXTERNAL_STORAGE 权限)中,并参考示例 (opens new window)将文件路径传入 SDK 中。

private fun generateAdditionalParams(): JsonObject {
    val additionalParams = JsonObject()

    ......

    val customWakeUp = JsonObject()
    customWakeUp.addProperty("wakeup_res_path", WAKEUP_RES_PATH)
    additionalParams.add("ivw", customWakeUp)

    return additionalParams
}

其中,WAKEUP_RES_PATH 代表唤醒资源的路径。

完成了以上步骤,SDK 启动后你可以自定义的唤醒词进行语音唤醒。

# 界面交互、或按键触发状态变更

根据设备能力API的部分要求,设备的一些状态改变(音量、播放暂停等)需要向服务端上报事件通知更改,故接入 SDK 时我们预先实现了上报事件的各个逻辑,但是你在接入 SDK 的时候,应当通过调用我们的接口来对设备部分状态进行更改。例如

# 改变音量

Demo 中实现了这样一套音量控制逻辑:

  • 在 Demo 应用内,按下音量键的事件会被直接拦截,将对应的音量变化直接同步到 SDK 中,在这里 (opens new window)可以看到一个减音量的参考实现。其中关键的设置音量代码为
iFLYOSManager.getInstance().executeSetVolume(volume) // volume 从 0 到 100

# 静音/取消静音

与改变音量类似,对应的操作 (opens new window)可以通过调用

iFLYOSManager.getInstance().executeSetMuted(isMuted) // true 则静音,否则取消

Demo 中可通过顶部设置框点击音量图标来触发静音/取消静音。

# 播放控制

Demo 中实现了一个简单的、用于显示正在播放的媒体的页面,主要逻辑代码位于 PlayerInfoFragment (opens new window) 中,其中真正触发播放控制的是以下代码

iFLYOSManager.getInstance().executeCommand(command)

Command 作为枚举类型,你可以看到其中包含以下成员

enum class Command) {
    CMD_PLAY_PLAY("play"), // 开始播放
    CMD_PLAY_PAUSE("pause"), // 暂停播放
    CMD_PLAY_NEXT("next"), // 下一曲
    CMD_PLAY_PREVIOUS("previous"), // 上一曲


    CMD_VOLUME_UP("up"), // 音量增
    CMD_VOLUME_DOWN("down"), // 音量减

    CMD_ALERT_STOP("stop_alert"); // 停止闹钟
}

传入对应的 Command 可以触发对应的操作。

建议

Command 中的 CMD_VOLUME_UPCMD_VOLUME_DOWN 会触发一个默认的音量增减,每次变化步长为 10 ,由于不同的设备,支持的调整步长并不一定都是 100/10,所以可能出现调整音量后取整不一致的问题,故我们推荐不要使用这两个值,而是自己换算每一步的步长,通过改变音量中的描述来改变音量。

(例如,若设备只支持 15 个音量步长,那设备本身按音量键变化应是 100/15,这时候出现除不尽的情况,导致 SDK 中的音量状态和真实音量有少许误差)

# 识别结果(转写结果)回调

startIFlyOS 中传入了 iflyosListener 之后,SDK 会对此对象回调分发事件。识别结果的回调也在其中。可以参考此处 (opens new window)实现获取转写结果进行界面展示。

# 一个简易的 OTA 实现

此实现为一个简易的检查更新实现,其中运用到的接口全部来自 自动更新API

接入此 module 主要有几个点。

  1. 检查服务端保存的最新安装包。此部分已在 OtaService.kt (opens new window) 中实现。其中包含了缓存文件、检查 APK 文件是否损坏、APK 版本与服务端包 id 对应关系等功能。

  2. 主模块中需要实现安装 APK 的能力。Demo 中主模块实现了需要用户点击确认安装的通用实现方式,若你可以对音箱的 ROM 进行定制,那么赋予 APP 系统权限,并使用系统权限来安装更新是一个更好的选择。

  3. 请注意每一次在服务端更新新的安装包时,需要保证 versionCode 应当比旧版本的数值大,Demo 中的实现会通过 versionCode 与包 id 的对应关系向服务端上报是否已更新。

# 自定义接入

Demo 的提供是为了方便开发者快速接入,可以在基本功能完备的情况下,只修改 UI 即可达到接入 iFLYOS 的目的。

但考虑到部分开发者需要高度定制化、自定义的接入 iFLYOS,我们也提供相关的介绍说明。我们只建议有较深厚的编程基础的开发者参考以下章节来接入。(在谷歌力推 kotlin 的大环境下,我们建议使用 kotlin 接入 SDK,下文的示例也都将使用 kotlin

# 引入主体模块

Android SDK 的封装,是将 Linux SDK 作为独立进程运行在 Android 设备上,并且通过 IPC 与其进行通讯,进行指令控制和状态同步。SDK 模块在 libs (opens new window) 可以直接下载,将 SDK 模块引入到你的项目中。

为 module 加入必要的依赖

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation(name:'ivs-android-sdk2', ext:'aar')

    implementation 'com.google.code.gson:gson:2.8.5'
    implementation 'com.squareup.okio:okio:2.0.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'
    implementation 'com.squareup.okhttp3:okhttp:3.11.0'
    implementation 'com.squareup.okhttp3:okhttp-urlconnection:3.11.0'
    implementation 'com.squareup.retrofit2:retrofit:2.4.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.4.0'

    implementation 'com.google.android.exoplayer:exoplayer-core:2.10.1'
    implementation 'com.google.android.exoplayer:exoplayer-dash:2.10.1'
    implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.10.1'
    implementation 'com.google.android.exoplayer:exoplayer-hls:2.10.1'

    // transform mp3 to pcm
    implementation 'com.googlecode.soundlibs:jlayer:1.0.1.4'
    ......
}

添加 JDK8 编译支持

android {
    ......

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    ......
}

# 创建 iFLYOSManager 对象

iFLYOSManager 类实现了单例模式,提供了两个获取实例的方式

val manager = iFLYOSManager.getInstance() // 获取当前实例,若为空则创建新实例

// or

val manager = iFLYOSManager.currentInstance() // 获取当前实例,若未创建过则返回 null

根据两种不同的获取实例方式,你可以判断是否 iFLYOSManager 被销毁,据此执行不同的逻辑处理。

创建 iFLYOSManager 的实例之后,应对实例进行初始化。

manager.init(context, clientId, configPath, iflyosListener, additionalConfigParams)

iflyosListener 回调的参数含义参考附录

若获取实例前已经初始化过,则不建议重新初始化一遍。 如果需要更新配置参数(例如唤醒词路径等),则建议使用以下函数来更新

manager.updateParams(additionalConfigParams)

注意

SDK 中包含了一系列配置参数的默认设置,这些设置会保存在初始化传入的 configPath 中,SDK 从该路径读取设置。当 additionalConfigParams 为空时,configPath 可以查看默认的配置都有哪些。additionalConfigParams 不为空时,则以此参数中的 json 字段优先。updateParams 同理。

# 授权

触发登录到 iFLYOS

manager.loginIvs()

在调用之后,SDK 会请求服务端获取授权 URL,并在 iflyosListeneronEvent 回调中,通过 iFLYOSEvent.EVENT_CBL_CODE 回调。在 Demo 中通过这里回调的字符串参数的第一行生成了二维码。

使用小飞在线扫码授权后,在 iflyosListeneronEvent 回调中会收到 iFLYOSEvent.EVENT_CLIENT_AUTH_STATE_CHANGE 回调,参数中第二行为 OK 时即代表授权成功。

# 启动服务

manager.startIFlyOS()

调用启动之后,SDK 会根据授权成功的 Token 去请求连接到 iFLYOS。连接成功后,在 iflyosListeneronEvent 回调中会收到 iFLYOSEvent.EVENT_CONNECTION_STATUS_CHANGED 回调,连接状态更新为 CONNECTED

此后即可通过语音唤醒使用。

何时启动服务

if (manager.isConnectedIvs) { // 若 SDK 已连接过 iFLYOS
    manager.initializeIvs()
} else { // 否则触发登录操作
    manager.loginIvs()
}

这里的流程有两种情况

  1. 若已连接过 iFLYOS,那么调用 initializeIvs() 之后会触发 SDK 刷新 Token。那么在收到 iFLYOSEvent.EVENT_AUTH_REFRESHED 事件之后,就可以触发 startIFlyOS() 启动服务

  2. 若未连接过 iFLYOS,那么调用登录后,会收到 iFLYOSEvent.EVENT_CLIENT_AUTH_STATE_CHANGE 回调 AuthState.OK,并且也会收到 iFLYOSEvent.EVENT_AUTH_REFRESHED,此时即可触发 startIFlyOS()

# 爱奇艺语音版 (试验性)

首先,体验这一特性需要保留此包名下 (opens new window)的相关代码。然后设备上需要安装特定版本的爱奇艺语音版应用(如需在你的设备中引入,请联系商务)。

使用这一功能需要在语音请求前同步当前爱奇艺应用的前后台、是否已安装等状态。在 Demo 中使用以下代码更新,如果你要自定义这部分代码,可用作参考

val packages = context.packageManager.getInstalledPackages(0)
val installedPkgName = HashSet<String>()
if (!packages.isNullOrEmpty()) {
    packages.map {
        if (it.packageName == ExternalVideoAppConstant.PKG_IQIYI_TV
                || it.packageName == ExternalVideoAppConstant.PKG_IQIYI_SPEAKER) {
            installedPkgName.add(it.packageName)
        }
    }
}
extVideoAppDirectiveHandler.resetAppStates(installedPkgName.toTypedArray())

Demo 会通过 AppStateObserver (opens new window) 来更新前后台应用的状态,用以实时更新语音请求的上下文状态。由于 Android 系统限制,在 Android L 之后需要利用读取应用使用情况来实现。

Demo 在应用列表更新(Intent.ACTION_PACKAGE_ADDED,Intent.ACTION_PACKAGE_REPLACED,Intent.ACTION_PACKAGE_REMOVED 广播)时也会更新上下文。服务端根据上下文,对用户说的话做出不同的回应。

# 附录

事件 参数
识别结果回调 EVENT_CLIENT_INTER_MEDIA_TEXT 识别结果文本。需要在设备控制台中开启逐字返回。
会话状态变更 EVENT_CLIENT_DIALOG_STATE_CHANGE 第一行: 前一个会话状态
第二行: 当前会话状态
具体值参考 iFLYOSClient.DialogState
授权状态变更 EVENT_CLIENT_AUTH_STATE_CHANGE 第一行: 前一个授权状态
第二行: 当前授权状态
具体值参考 iFLYOSClient.AuthState
设备被语音唤醒 EVENT_SPEECH_RECOGNIZER_WAKEUP
引擎事件透传 EVENT_ENGINE_RAW *引擎事件透传较为繁琐,如需要了解含义请联系技术支持
设备被解除绑定 EVENT_REVOKE_AUTHORIZATION
设备录音音量回调 EVENT_VOLUME_CHANGE 音量字符串。目前由 SpeechRecognizerHandler (opens new window) 回调,你可以自定义计算音量的方式
iFLYOS 连接状态变更 EVENT_CONNECTION_STATUS_CHANGED 第一行: 连接状态
第二行,连接状态变更原因
具体值参考 iFLYOSClient.ConnectionStatusiFLYOSClient.ConnectionChangedReason
授权URL回调 EVENT_CBL_CODE 第一行: 授权URL
第二行: 授权码
授权URL过期 EVENT_CBL_CODE_EXPIRED
创建录音失败 EVENT_CREATE_AUDIO_RECORD_FAILED 原因文本。目前仅在 SpeechRecognizerHandler (opens new window) 中调用,如果你使用自定义的录音模块,可以自定义分发不同的错误原因,并针对不同的原因做用户提示
设备Token过期 EVENT_AUTH_TOKEN_EXPIRED

# 接入常见问题

问:打开 Android Studio 项目网络失败?

答:需要能够正常访问 jCenter、Google Maven 的网络连接。

问:网络确实连上了,但是绑定帐号提示「网络异常,请重试。」

答:请检查设备的时间是否正确。不正确的时间会导致与服务端的 HTTPS 通讯失败。

问:二维码显示正常,但是绑定设备提示「无效应用。」

答:ShowCore 中内置的 Client Id 是仅作为测试用的,如果此 Id 失效,请在参考控制台管理设备介绍获取你创建的自定义设备的 Client Id,进行替换,并完成如下操作:

  1. 在设备接入控制台-设备能力页,打开【有屏配网】能力。
  2. 在设备接入控制台-测试调试页,填写你绑定设备时使用的手机号。