# ShowCore

TIP

ShowCore 项目是使用 iFLYOS 的 EVS 协议开发的、运行在 Android 平台的通用语音交互 Launcher App。

本文档将介绍 ShowCore 在目前的技术框架下应用从启动到可以进行语音交互的流程。以下内容基于已经进行了设备授权的前提。

# 开始启动

ShowCore 作为一个 Launcher App,期望在智能音箱中作为默认的 Launcher 应用,以期可以在开机时便启动应用。

启动 EvsLauncherActivity (opens new window) 后,会在 onResume 回调中检查权限获取的状态。权限概述

确认所有权限都获取之后,进行初始化工作。

  1. 初始化悬浮层。 FloatingService (opens new window)

    为了保证一些 UI 交互可以在任何应用之上展示给用户,我们采用悬浮层的方式来实现这些交互。

    用于在悬浮层中展示自定义视图的类是 FloatingService (opens new window)。目前在悬浮层中添加的视图有:

    视图 说明
    模板展示视图 用于根据 模板展示端能力 (opens new window) 下发的不同类型展示丰富的模板视图。
    闹钟关闭视图 用于在响闹钟的时候,在任意界面都可以弹出提醒,并提供关闭选项。
    语音唤醒按钮 用于用户期望使用触控唤醒设备进行语音交互的场景,按钮可全局拖动、自动停靠左右边缘。
    文本转写(IAT)展示视图 用于默认状态下语音服务的识别内容。
    语音播报(TTS)文本展示视图 用于展示设备所播报的文本。
    背景录音展示视图 用于展示背景录音状态下语音服务的识别内容。
    顶部通知展示视图 用于对一些状态变更进行全局通知。
    音量、亮度变化展示视图 用于当用户语音请求修改亮度、音量时,对用户进行设置变更的 UI 上的提示。
    下拉控制栏视图 用于在全局都可以快速展开的便捷操作,例如回到首页、打开设置等。
    下拉控制栏手势监听控件 用于监听下拉控制的手势。
    左边缘滑动返回 UI 控件 用于展示滑动返回的动效。
    左边缘滑动返回手势监听控件 用于监听左边缘滑动返回的手势。
    连接中状态展示视图 用于展示 EVS 连接中的状态。

    如果开发者有自定义 ShowCore 的需求,并且期望增加部分可以在所有应用上层展示的控件,则可以参考此类进行自定义二次开发,初始化函数为 initOverlay()

  2. 初始化 EVS 引擎服务。EngineService (opens new window)

    EVS 引擎服务(下称 EngineService )继承于 EVS SDK 中的 EvsService,并根据要求实现了部分对应的端能力。

    EngineService 负责管理与 EVS 发生交互的操作,且可以接收到 EVS 状态变更的回调。

    在网络正常且已经授权的情况下,EvsLauncherAcitvity 在对 EngineService 进行绑定后即调用 connectEvs() 函数触发连接 EVS 服务建立长连接的操作。EVS 交互协议文档可参考 EVS 接入 (opens new window)

    对于 EVS 定义的设备端能力,要求必须实现的部分在 EVS SDK 中已经内置了默认实现,EngineService 通过复写 EvsService 提供的 override[Interface] 函数来实现自定义的端能力对象。在 EvsServiceonCreate 中我们可以看到目前支持的可复写的自定义端能力函数。在功能模块说明文档 (opens new window)中你可以看到这些端能力定义的含义及其必要性。

    class EvsService: Service() {
        // ...
    
        override fun onCreate() {
            super.onCreate()
    
            audioPlayer = overrideAudioPlayer()
            alarm = overrideAlarm()
            appAction = overrideAppAction()
            interceptor = overrideInterceptor()
            launcher = overrideLauncher()
            playbackController = overridePlaybackController()
            recognizer = overrideRecognizer()
            screen = overrideScreen()
            speaker = overrideSpeaker()
            system = overrideSystem()
            template = overrideTemplate()
            videoPlayer = overrideVideoPlayer()
            wakeWord = overrideWakeWord()
        }
    
        // ...
    }
    

    例如 EngineService 实现的其中一个自定义端能力 audio_player 音频播放器端能力 (opens new window)

    class EngineService: EvsService() {
        // ...
    
        override fun overrideAudioPlayer(): AudioPlayer {
            return EvsAudioPlayer.get(this)
        }
        
        // ...
    }
    

    其中的 EvsAudioPlayer (opens new window) 即一个完整的音频播放器端能力实现。其中播放音频的实现方式基于谷歌的 ExoPlayer (opens new window)

  3. 初始化 Fragment 管理

    TIP

    ShowCore 引用了开源项目 Fragmentation (opens new window) 进行页面管理,全局仅有唯一入口 EvsLauncherActivity,从 Activity 进入后再进行意图分发。

    初始化时, Activity 根据 EVS SDK 中 AuthDelegate 的返回结果进行判断应当渲染什么界面。所有的界面都由 Fragment 来实现,基于实现了 ISupportFragmentBaseFragment (opens new window)

    BaseFragment 中实现了部分 Fragmentation 提供的可供自定义的配置,如果你想知道如何去除或新增一些额外的自定义,请前往 Fragmentation 官方仓库 (opens new window) 查阅相关订阅。

  4. 初始化录音

    GlobalRecorder (opens new window) 中可以看到 ShowCore 自定义的录音实现,其中包含了 AudioRecord 的标准初始化,以及订阅和分发录音数据的逻辑。

    对于语音唤醒的默认实现,目前采用了讯飞官方提供的 70 版本单路音频语音唤醒引擎版本(在 app/src/main/cpp (opens new window) 中可以参考 JNI 调用 ivw 引擎的相关业务逻辑),并且提供了我们深定制的默认唤醒词资源。你可以通过 自定义唤醒词工具 (opens new window) 创建唤醒效果较差、仅供体验用的浅定制的唤醒词资源。如果对唤醒效果有更高的要求,请联系我们的技术支持或商务沟通创建深定制版本的唤醒词资源,以获得最佳的语音唤醒体验。

    在普通的 Android 设备上,唤醒、录音效果可能都不太理想,因为普通设备不具备效果优化的麦克风阵列、且没有经过讯飞定制的 CAE 引擎进行优化处理。如对定制化的麦克风阵列或者 CAE 引擎感兴趣,请联系我们的技术支持或商务沟通交流进一步合作。

    如果你需要自行开发接入其他的录音设备,那么我们建议你通过修改 GlobalRecorder 已定义好的相关函数进行初始化和录音数据拾取,这将是对项目代码改动最小的接入方式。

经过以上的基础初始化之后,ShowCore 应当已经处于一个可交互的正常工作状态了。

# 运行过程

在 Android 的内存管理机制中,Activity 在内存不足的情况下是有可能被销毁的。为了能够保证后台运行过程中,即便主界面被销毁,语音交互也能正常执行,我们通常将所有语音交互相关的回调优先写在 EngineService 中(例如 EngineService 中的 templateCallbackaudioPlayerListener 等回调监听),而只把与界面强相关的回调写在 EvsLauncherActivity ,以此保证后台运行过程中的语音交互可靠性。

# EVS 交互

与 EVS 相关的交互流程都封装在 EVS SDK 中,外部调用只需要对对应端能力的函数来实现。

EnginerService 可以通过 get[interface] 获得各个端能力的实现对象。以下是一个发起语音请求的示例。语音请求协议定义 (opens new window)

val recognizer = engineService.getRecognier() // 获得识别器对象
recognizer.sendAudioIn(null) // 发送音频请求

TIP

上述方式只是一个简单的示例,实际业务中发起语音请求的代码在 EngineService 中已封装为 fun sendAudioIn(wakeUpJson, replyKey)

# 界面交互

ShowCore 中的 Activity 都基于 BaseActivity (opens new window),所有的 Fragment 都基于 BaseFragment (opens new window)。两者都可以调用 start(fragment: BaseFragment) 在应用中启动一个新的界面。

# 悬浮层调用

所有对悬浮层调用都可以归类成对 FloatingService (opens new window)startService 操作,其中不同的只有 actionextras 等参数的区分。

例如,在设备接到模板展示响应 (opens new window)时,在 TemplaterenderTemplate 回调可以获得模板展示下发的 payload 参数,并将这些参数通过 startService 的方式启动 FloatingService 触发展示模板视图。

override fun renderTemplate(payload: String) {
    // ...

    val intent = Intent(baseContext, FloatingService::class.java)
    intent.action = FloatingService.ACTION_RENDER_TEMPLATE
    intent.putExtra(FloatingService.EXTRA_PAYLOAD, payload)
    ContextWrapper.startServiceAsUser(baseContext, intent, "CURRENT")

    // ...
}

# Keep Alive 模块

在运行过程中,ShowCore 应用可能因为某些 Bug 导致了崩溃,虽然作为桌面应用,往往 ShowCore 可以在强行停止后马上恢复启动,但为了提高 ShowCore 的存活可能性,我们使用另一个单独的应用来保证 ShowCore 在被强行停止之后仍可以恢复启动。

请注意,此模块仅仅是一个最基本、最简单的保活模块,可靠性实质上不具备非常大的保障。

在此模块中,不具有任何的界面,仅提供一个维护状态的 ServiceService 中会在被 ShowCore 启动之后,监听每一段时间间隔内 ShowCore 是否有对 Keep Alive 的服务发送 “PING”,如果没有,则认为 ShowCore 可能已经崩溃,此时 Keep Alive 的 Service 就会尝试打开 ShowCore 应用。

# 部分 Intent 外部调用方式

TIP

本文档仅针对运行了 EVS 带屏 App(下文称为 ShowCore)的、基于 Android 系统的带屏音箱。本文档建议具有一定 Android 开发基础的开发者阅读使用。

为了使开发者可以完整开发一个具备智能交互能力的独立 App,我们增添若干可供外部 Intent 调用的功能,以便于第三方 App 可以与语音交互能力联动,丰富用户体验。

以下示例皆通过 Kotlin 展示样例,Java 调用方式亦可,仅供参考。

# 第三方 App 主动触发 ShowCore 的方式

#EngineService 进行操作触发的请求

以下只是一段简单的示例,实际调用请根据下文表格中不同能力的调用参数进行相应的修改。

val intent = Intent("SOME_DEFINED_ACTION")
intent.setClassName("com.iflytek.cyber.iot.show.core", "com.iflytek.cyber.iot.show.core.EngineService")
intent.putExtra("EXTRA_KEY", "EXTRA_VALUE")
context.startService(intent)

TIP

  • 下表中所有 action 都有统一前缀 com.iflytek.cyber.iot.show.core.action,这里定义为 ACTION_PREFIX 以节省篇幅。

  • extra项中表示为 json 格式,实际上应根据类型传参,例如{"query": "今天天气怎么样"}则对应调用intent.putExtra("query", "今天天气怎么样")`。

意图 action extras 备注
发起语音请求 $ACTION_PREFIX.SEND_AUDIO_IN
发起文本请求 $ACTION_PREFIX.SEND_TEXT_IN {"query": "今天天气怎么样"}
请求进行文本播报*(注释 1)* $ACTION_PREFIX.PLAY_TTS {"tts_text": "这是一段测试语音播报", "tts_path": "/sdcard/tts.mp3"} tts_texttts_path 同时存在,则以 tts_path 优先
  • 注释 1:设备端主动请求播报 TTS 能力并非默认具备的能力,需要联系商务开通专业版权限。EVS 协议中实际发生的请求为 audio_player.tts.text_in,见文本合成相关文档 (opens new window)

#EvsLauncherActivity 进行操作触发页面跳转

以下只是一段简单的示例,实际调用请根据下文表格中不同页面的调用参数进行相应的修改。

val intent = Intent()
intent.setClassName("com.iflytek.cyber.iot.show.core", "com.iflytek.cyber.iot.show.core.EvsLauncherActivity")
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.action = "SOME_ACTION"
intent.putExtra("EXTRA_KEY", "EXTRA_VALUE")
context.startActivity(intent)

TIP

下表中所有 action 都有统一前缀 com.iflytek.cyber.iot.show.core,这里定义为 ACTION_PREFIX 以节省篇幅。

页面 action 备注
主界面 $ACTION_PREFIX.OPEN_HOME
设置 $ACTION_PREFIX.OPEN_SETTINGS
WiFi 设置 $ACTION_PREFIX.OPEN_WLAN
二维码绑定 $ACTION_PREFIX.OPEN_AUTH 若已授权也可以打开此页面,扫码后会重新绑定设备
技能商店 $ACTION_PREFIX.OPEN_SKILLS
内容发现 $ACTION_PREFIX.OPEN_CONTENT
内容搜索 $ACTION_PREFIX.ACTION_OPEN_SEARCH
闹钟设置 $ACTION_PREFIX.OPEN_ALARM
检查更新 $ACTION_PREFIX.OPEN_CHECK_UPDATE
音乐播放器 $ACTION_PREFIX.START_PLAYER

# ShowCore 通知外部应用的广播

# 收到拦截器下发的自定义指令

设备接入平台中自定义前置拦截器可以针对自定义的场景对设备下发不同的自定义指令,甚至也可以将语义直接给到设备端,由设备进行处理。自定义拦截器介绍 (opens new window)

含义 action extra 中的 type extra 中的 payload
自定义指令 com.iflytek.cyber.iot.show.core.action.INTERCEPTOR_RESULT Custom 自定义指令的 payload
AIUI 语音 com.iflytek.cyber.iot.show.core.action.INTERCEPTOR_RESULT TransferSemantic AIUI 语义

# 常见问题

  1. ContextWrapper (opens new window) 的封装有什么含义?

    在部分可能出现的定制需求中,ShowCore 将作为系统应用集成到固件中,而作为系统应用,有许多 Context 相关的操作要求不允许以系统身份去执行,那么需要通过反射作为用户身份去执行保证不会抛出异常。

# 附录

# 部分界面及其代码对应关系

页面
主界面 MainFragment2 (opens new window)
应用中心 LauncherFragment2 (opens new window)
闹钟 AlarmFragment (opens new window)
内容发现 MediaFragment (opens new window)
设置 SettingsFragment (opens new window)
技能商店 SkillsFragment (opens new window)

# 权限概述

本表仅列出用户可感知的、动态申请的权限。

权限 用途
麦克风 用于进行语音唤醒和语音识别
定位 用于进行地理位置展示和天气预报
存储 用于部分需要进行文件选择的自定义功能
修改系统设置 用于修改亮度、音量等系统设置
读取应用使用情况 用于计算当前前台应用,服务端根据不同前台应用可进入不同技能处理
在其他应用之上显示内容 用于展示全局的下拉控制、语音唤醒悬浮按钮、模板展示等视图