#!/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= 指定目标设备,例如:" 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 [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 [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 [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}" }