Files
jiecheng-business/test/voice_helper/lib/common.sh
freeboss b7b5518834 feat: dev-5.1 分支基线(v1.3.35 发版后代码 + 5.1 声道改造文档)
- 以 main(ccc65af) 为基线提交当前工作区代码
- 新增 5.1 声道改造文档两份
- .gitignore 补充 .vscode/、.screenshots/
2026-11-02 19:25:12 +08:00

342 lines
12 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# common.sh - 语音协议广播测试公共函数
# 其他脚本通过 source "$(dirname "$0")/lib/common.sh" 加载
#
# 协议参考 docs/2026-08-22-语音影视接入可行性评估.md §6.1
set -o pipefail
# 加载 device.sh 设备配置(位于 common.sh 同级父目录)
# 必须在协议常量前 source,确保 ADB_SERIAL 已被设置
_DVH_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "${_DVH_LIB_DIR}/../device.sh" ]; then
# shellcheck disable=SC1091
source "${_DVH_LIB_DIR}/../device.sh"
fi
unset _DVH_LIB_DIR
# ===== 协议常量(与文档 §6.1 保持一致)=====
: "${ACTION_VOICE_COMMAND:=com.pukka.jiecheng.action.VOICE_COMMAND}"
: "${PERMISSION_VOICE:=com.pukka.jiecheng.permission.VOICE_COMMAND}"
# 应用 applicationId:默认商用版(tv.huashi.jetsenx.jetsenfilx.jsttv);
# 家用版 APK 安装时通过 APP_PKG=tv.huashi.jetsenx.jetsenfilx.jsttv.home 覆盖。
# 下方 RECEIVER_COMPONENT / SKYWORTH_RECEIVER_COMPONENT / TEST_APP_PKG 都基于 APP_PKG 派生。
: "${APP_PKG:=tv.huashi.jetsenx.jetsenfilx.jsttv}"
: "${RECEIVER_COMPONENT:=${APP_PKG}/com.pukka.jiecheng.voice.receiver.VoiceCommandReceiver}"
: "${TEST_APP_PKG:=${APP_PKG}}"
: "${MAIN_ACTIVITY_COMPONENT:=com.pukka.jiecheng/.MainActivity}"
: "${DEFAULT_SOURCE_APP:=com.test.voicehelper}"
: "${DEFAULT_PLATFORM:=vendorA}"
: "${DEFAULT_CALLBACK_ACTION:=com.test.voicehelper.action.VOICE_CALLBACK}"
: "${TEST_REQUEST_PREFIX:=vt-}"
# ===== 创维(skyworth)广播常量(SkyworthVoiceAdapter.java)=====
# action 与 extras key 与 SkyworthVoiceAdapter.ACTION_VOD / EXTRA_VIDEO_LIST 保持一致
: "${ACTION_SKYWORTH_VOD:=com.skyworth.smarthotel.action.voice.RecognizeResult.vod}"
: "${SKYWORTH_RECEIVER_COMPONENT:=${APP_PKG}/com.pukka.jiecheng.voice.adapter.SkyworthVoiceAdapter}"
# 白名单 sender(SkyworthVoiceAdapter.SENDER_WHITELIST),adb 投递时通过 --p 指定
# 发送方包名模拟创维 SDK,绕开白名单校验的测试见 skyworth_security_test.sh
: "${DEFAULT_SKYWORTH_SENDER:=com.skyworth.smarthotel}"
# 当开发期把 permission 临时降到 normal / 关闭时可置 1
: "${SKIP_PERMISSION_CHECK:=0}"
# 当本机 adb 上挂了多台设备时,通过 ADB_SERIAL 指定唯一目标,避免脚本误连
: "${ADB_SERIAL:=}"
if [ -n "${ADB_SERIAL}" ]; then
ADB="adb -s ${ADB_SERIAL}"
else
ADB="adb"
fi
# ===== 颜色输出(仅在交互终端生效)=====
if [ -t 1 ] && command -v tput >/dev/null 2>&1 && [ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]; then
C_RED=$(tput setaf 1); C_GRN=$(tput setaf 2); C_YEL=$(tput setaf 3)
C_CYN=$(tput setaf 6); C_RST=$(tput sgr0)
else
C_RED=""; C_GRN=""; C_YEL=""; C_CYN=""; C_RST=""
fi
log_info() { printf "%s[INFO]%s %s\n" "${C_CYN}" "${C_RST}" "$*"; }
log_ok() { printf "%s[OK]%s %s\n" "${C_GRN}" "${C_RST}" "$*"; }
log_warn() { printf "%s[WARN]%s %s\n" "${C_YEL}" "${C_RST}" "$*"; }
log_err() { printf "%s[ERR]%s %s\n" "${C_RED}" "${C_RST}" "$*" >&2; }
# ===== 校验 =====
require_adb() {
if ! command -v adb >/dev/null 2>&1; then
log_err "未找到 adb,请安装 Android Platform Tools 后重试"
exit 1
fi
if ! ${ADB} get-state >/dev/null 2>&1; then
log_err "adb 未连接设备,请先执行 adb connect / adb devices"
exit 1
fi
# 解析 adb devices 输出,仅保留 device 状态(排除 unauthorized / offline)
local online count
online=$(${ADB} devices | awk 'NR>1 && $2=="device" {print $1}')
count=$(printf '%s\n' "${online}" | grep -c .)
if [ -n "${ADB_SERIAL}" ]; then
if ! printf '%s\n' "${online}" | grep -qx "${ADB_SERIAL}"; then
log_err "指定的 ADB_SERIAL=${ADB_SERIAL} 未处于 device 状态,请检查 adb devices"
exit 1
fi
elif [ "${count}" -gt 1 ]; then
log_err "检测到 ${count} 个在线设备,请先 export ADB_SERIAL=<serial> 指定目标设备,例如:"
log_err " ADB_SERIAL=$(printf '%s' "${online}" | head -n1) ./jump_film.sh \"镖人\" true"
exit 1
else
# 只有一个设备时默认使用它,避免每次都要 export
export ADB_SERIAL="${online}"
log_info "自动选择唯一在线设备:${ADB_SERIAL}"
fi
}
check_receiver_exists() {
local found
found=$(${ADB} shell dumpsys package "${TEST_APP_PKG}" 2>/dev/null \
| grep -i "VoiceCommandReceiver" || true)
if [ -z "${found}" ]; then
log_warn "VoiceCommandReceiver 未在 ${TEST_APP_PKG} 中注册"
log_warn "请确认 voice 模块已实现并打包进 APK,预期 Broadcast 会被丢弃"
fi
}
# 校验创维 Adapter 已注册(Manifest 中 SkyworthVoiceAdapter + action)
check_skyworth_adapter_exists() {
local found
found=$(${ADB} shell dumpsys package "${TEST_APP_PKG}" 2>/dev/null \
| grep -i "SkyworthVoiceAdapter" || true)
if [ -z "${found}" ]; then
log_warn "SkyworthVoiceAdapter 未在 ${TEST_APP_PKG} 中注册"
log_warn "请确认创维语音 Adapter 已打包进 APK,预期 Broadcast 会被丢弃"
fi
}
# ===== 工具函数 =====
gen_request_id() {
local rand
rand=$(printf '%05d' "$((RANDOM % 100000))")
printf '%s%d-%s' "${TEST_REQUEST_PREFIX}" "$(date +%s)" "${rand}"
}
json_escape() {
local v="$1"
v="${v//\\/\\\\}"
v="${v//\"/\\\"}"
printf '%s' "${v}"
}
# JUMP / SEARCH payload(全部字段均为字符串)
# 用法: build_jump_payload "FILM" "filmId=xxx" "filmName=yyy" "autoPlay=true"
build_jump_payload() {
local jump_type="$1"; shift
local first=1 json="{"
json="${json}\"jumpType\":\"${jump_type}\""
while [ $# -gt 0 ]; do
local kv="$1" key val
key="${kv%%=*}"
val="${kv#*=}"
val=$(json_escape "${val}")
json="${json},\"${key}\":\"${val}\""
shift
done
json="${json}}"
printf '%s' "${json}"
}
build_search_payload() {
local first=1 json="{"
while [ $# -gt 0 ]; do
local kv="$1" key val
key="${kv%%=*}"
val="${kv#*=}"
val=$(json_escape "${val}")
if [ $first -eq 1 ]; then first=0; else json="${json},"; fi
json="${json}\"${key}\":\"${val}\""
shift
done
json="${json}}"
printf '%s' "${json}"
}
# CONTROL payload:第一个参数是 action,其余 key=value 中 value 视为数字字面量
# 用法: build_control_payload "PAUSE"
# build_control_payload "SEEK_FORWARD" "seconds=10"
build_control_payload() {
local action="$1"; shift
local json="{\"action\":\"${action}\""
while [ $# -gt 0 ]; do
local kv="$1" key val
key="${kv%%=*}"
val="${kv#*=}"
json="${json},\"${key}\":${val}"
shift
done
json="${json}}"
printf '%s' "${json}"
}
# ===== 核心发送函数 =====
# 用法: send_voice_broadcast <commandType> <payloadJson> [requestId] [sourceApp] [sourcePlatform] [callbackAction]
send_voice_broadcast() {
local command_type="$1"
local payload_json="$2"
local request_id="${3:-$(gen_request_id)}"
local source_app="${4:-${DEFAULT_SOURCE_APP}}"
local source_platform="${5:-${DEFAULT_PLATFORM}}"
local callback_action="${6:-${DEFAULT_CALLBACK_ACTION}}"
require_adb
local timestamp
timestamp=$(date +%s%3N 2>/dev/null || echo $(( $(date +%s) * 1000 )))
log_info "commandType=${command_type} requestId=${request_id}"
log_info "sourceApp=${source_app} platform=${source_platform}"
log_info "payload=${payload_json}"
# adb shell 接收一个字符串后整体交给设备 shell 执行,避免 adb 自身再做 token 化
${ADB} shell "am broadcast \
-a '${ACTION_VOICE_COMMAND}' \
-n '${RECEIVER_COMPONENT}' \
--es requestId '${request_id}' \
--es sourceApp '${source_app}' \
--es sourcePlatform '${source_platform}' \
--es commandType '${command_type}' \
--es payload '${payload_json}' \
--es callbackAction '${callback_action}' \
--el timestamp '${timestamp}' \
--include-stopped-packages" 2>&1 | sed 's/^/ [adb] /'
log_ok "已投递(requestId=${request_id})"
}
# ===== 创维(skyworth)广播 =====
# 模拟创维 SDK 投递 RecognizeResult.vod 广播给 SkyworthVoiceAdapter。
#
# 关于 videoList 的说明:
# 创维 SDK 用 --es 投递 JSON 数组字符串(与 SkyworthVoiceAdapter.resolveVideoList 对应):
# am broadcast -a com.skyworth.smarthotel.action.voice.RecognizeResult.vod \
# --es videoList '["哪吒之魔童降世","我是哪吒"]'
# Adapter 取第一个非空元素,多值场景可模拟创维候选列表。
#
# 可选参数(通过环境变量控制,均为可选):
# SKYWORTH_SKIP_COMPONENT=1 省略 -n 参数(不显式指定 Receiver 组件)
# SKYWORTH_SKIP_SENDER=1 省略 -p 参数(不指定 sender 包名,Adapter 内部用白名单默认值兜底)
#
# 用法: send_skyworth_broadcast <filmName> [senderPkg] [extraKv...]
# filmName 候选影片名(videoList[0])
# senderPkg 发送方包名,默认 DEFAULT_SKYWORTH_SENDER(白名单内);传空串或 SKYWORTH_SKIP_SENDER=1 时省略 -p
# extraKv 透传给 am broadcast 的附加 --es 参数,形如 "key=value"
send_skyworth_broadcast() {
local film_name="$1"
local sender_pkg="${2:-${DEFAULT_SKYWORTH_SENDER}}"
shift 2 2>/dev/null || shift $# # 兼容只传 filmName 的情况
require_adb
# videoList 为 JSON 数组字符串(--es),与创维 SDK 真实投递格式一致
local video_list_json
video_list_json="[\"$(json_escape "${film_name}")\"]"
log_info "action=${ACTION_SKYWORTH_VOD}"
if [ -n "${SKYWORTH_SKIP_SENDER}" ] || [ -z "${sender_pkg}" ]; then
log_info "sender=SKIP(-p omitted) videoList=${video_list_json}"
else
log_info "sender=${sender_pkg} videoList=${video_list_json}"
fi
if [ -n "${SKYWORTH_SKIP_COMPONENT}" ]; then
log_info "component=SKIP(-n omitted)"
fi
# 附加 extras(可选):默认注入 autoPlay=true(创维链路默认自动播片)
# 用户传的 extraKv 会追加在后面,按 am broadcast 规则后者覆盖前者,
# 因此调用方通过 "autoPlay=false" 即可临时关闭自动播放。
local extra_args="--es autoPlay 'true' "
local kv
for kv in "$@"; do
local key val
key="${kv%%=*}"
val="${kv#*=}"
extra_args="${extra_args}--es ${key} '${val}' "
done
# 组装 -n 和 -p(按开关可选)
local n_arg=""
local p_arg=""
if [ -z "${SKYWORTH_SKIP_COMPONENT}" ]; then
n_arg="-n '${SKYWORTH_RECEIVER_COMPONENT}' "
fi
if [ -z "${SKYWORTH_SKIP_SENDER}" ] && [ -n "${sender_pkg}" ]; then
p_arg="-p '${sender_pkg}' "
fi
${ADB} shell "am broadcast \
-a '${ACTION_SKYWORTH_VOD}' \
${n_arg}${p_arg}--es videoList '${video_list_json}' \
${extra_args}--include-stopped-packages" 2>&1 | sed 's/^/ [adb] /'
log_ok "已投递创维 VOD 广播(filmName=${film_name})"
}
# ===== 应用级操作 =====
launch_main_activity() {
log_info "拉起 MainActivity:${MAIN_ACTIVITY_COMPONENT}"
${ADB} shell am start -n "${MAIN_ACTIVITY_COMPONENT}" >/dev/null 2>&1
}
force_stop_app() {
log_info "强制停止应用:${TEST_APP_PKG}"
${ADB} shell am force-stop "${TEST_APP_PKG}" >/dev/null 2>&1
}
# 验证当前前台 Activity 是否为预期页面(按类名片段匹配)
# 用法: wait_for_activity <ActivitySimpleName> [timeoutMs]
# 返回 0 表示命中,1 表示超时
wait_for_activity() {
local activity_name="${1}"
local timeout_ms="${2:-10000}"
local interval_ms=500 elapsed=0
log_info "等待前台 Activity 命中 ${activity_name}(最长 ${timeout_ms}ms)..."
while [ ${elapsed} -lt "${timeout_ms}" ]; do
local top
top=$(${ADB} shell "dumpsys window 2>/dev/null | grep -oE 'mCurrentFocus=[^}]+' | head -n 1" || true)
case "${top}" in
*"${activity_name}"*)
log_ok "前台命中:${top}"
return 0
;;
esac
sleep "$(awk "BEGIN { print ${interval_ms}/1000 }")"
elapsed=$((elapsed + interval_ms))
done
log_warn "等待超时(${timeout_ms}ms),当前前台:$(${ADB} shell "dumpsys window 2>/dev/null | grep -oE 'mCurrentFocus=[^}]+' | head -n 1" || true)"
return 1
}
wait_for_app_ready() {
local timeout_ms="${1:-15000}"
local interval_ms=300 elapsed=0
log_info "等待应用启动链路完成(最长 ${timeout_ms}ms)..."
while [ ${elapsed} -lt "${timeout_ms}" ]; do
local top
top=$(${ADB} shell "dumpsys window 2>/dev/null | grep -oE 'mCurrentFocus=[^}]+' | head -n 1" || true)
case "${top}" in
*"${TEST_APP_PKG}"* | *"com.pukka.jiecheng"* | *"${MAIN_ACTIVITY_COMPONENT%%/*}"*)
log_ok "应用已就绪:${top}"
return 0
;;
esac
sleep "$(awk "BEGIN { print ${interval_ms}/1000 }")"
elapsed=$((elapsed + interval_ms))
done
log_warn "等待超时(${timeout_ms}ms),继续执行"
return 1
}
print_header() {
printf "\n%s================ %s ================%s\n" "${C_CYN}" "$*" "${C_RST}"
}