342 lines
12 KiB
Bash
342 lines
12 KiB
Bash
|
|
#!/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}"
|
|||
|
|
}
|