feat: dev-5.1 分支基线(v1.3.35 发版后代码 + 5.1 声道改造文档)
- 以 main(ccc65af) 为基线提交当前工作区代码
- 新增 5.1 声道改造文档两份
- .gitignore 补充 .vscode/、.screenshots/
This commit is contained in:
342
test/voice_helper/lib/common.sh
Normal file
342
test/voice_helper/lib/common.sh
Normal file
@@ -0,0 +1,342 @@
|
||||
#!/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}"
|
||||
}
|
||||
Reference in New Issue
Block a user