Files

342 lines
12 KiB
Bash
Raw Permalink Normal View History

#!/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}"
}