Compare commits
14 Commits
contacts-v
...
contacts-v
| Author | SHA1 | Date | |
|---|---|---|---|
| 04fd7e74fa | |||
| cdf51637d3 | |||
| 4ae2133d0f | |||
| cce4a643e7 | |||
| 61cfa7f3ff | |||
| cb9ad1dc57 | |||
| 98c334f442 | |||
| 47c328cd25 | |||
| 7134d4e1c8 | |||
| 84c6271310 | |||
| a4ab864381 | |||
| 217a27cbcd | |||
| 778a1bc98e | |||
| 561abd2398 |
87
.github/workflows/android.yml
vendored
87
.github/workflows/android.yml
vendored
@@ -1,87 +0,0 @@
|
|||||||
name: Android CI
|
|
||||||
|
|
||||||
# 触发器
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- *-beta
|
|
||||||
pull_request:
|
|
||||||
tags:
|
|
||||||
- *-beta
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
# 设置 JDK 环境
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: set up JDK 11
|
|
||||||
uses: actions/setup-java@v3
|
|
||||||
with:
|
|
||||||
java-version: '11'
|
|
||||||
distribution: 'temurin'
|
|
||||||
cache: gradle
|
|
||||||
|
|
||||||
- name: Grant execute permission for gradlew
|
|
||||||
run: chmod +x gradlew
|
|
||||||
|
|
||||||
# 获取应用打包秘钥库
|
|
||||||
- name: Checkout Android Keystore
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
repository: zhangsken/keystore # 存储应用打包用的 keystore 的仓库(格式:用户名/仓库名)
|
|
||||||
token: ${{ secrets.APP_SECRET_TOKEN_1 }} # 连接仓库的 token , 需要单独配置
|
|
||||||
path: keystore # 仓库的根目录名
|
|
||||||
|
|
||||||
# 打包 Stage Release 版本应用
|
|
||||||
- name: Build with Gradle
|
|
||||||
run: bash ./gradlew assembleBetaRelease
|
|
||||||
# 创建release
|
|
||||||
- name: Create Release
|
|
||||||
id: create_release
|
|
||||||
uses: actions/create-release@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.APP_SECRET_TOKEN_1 }}
|
|
||||||
# GitHub 会自动创建 GITHUB_TOKEN 密码以在工作流程中使用。
|
|
||||||
# 您可以使用 GITHUB_TOKEN 在工作流程运行中进行身份验证。
|
|
||||||
# 当您启用 GitHub Actions 时,GitHub 在您的仓库中安装 GitHub 应用程序。
|
|
||||||
# GITHUB_TOKEN 密码是一种 GitHub 应用程序 安装访问令牌。
|
|
||||||
# 您可以使用安装访问令牌代表仓库中安装的 GitHub 应用程序 进行身份验证。
|
|
||||||
# 令牌的权限仅限于包含您的工作流程的仓库。 更多信息请参阅“GITHUB_TOKEN 的权限”。
|
|
||||||
# 在每个作业开始之前, GitHub 将为作业提取安装访问令牌。 令牌在作业完成后过期。
|
|
||||||
with:
|
|
||||||
tag_name: ${{ github.ref }}
|
|
||||||
release_name: Release ${{ github.ref }}
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
|
|
||||||
# 获取 APK 版本号
|
|
||||||
- name: Get Version Name
|
|
||||||
uses: actions/github-script@v3
|
|
||||||
id: get-version
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const str=process.env.GITHUB_REF;
|
|
||||||
return str.substring(str.indexOf("v"));
|
|
||||||
result-encoding: string
|
|
||||||
# 上传至 Release 的资源
|
|
||||||
- name: Upload Release Asset
|
|
||||||
id: upload-release-asset
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.APP_SECRET_TOKEN_1 }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }} # 上传网址,无需改动
|
|
||||||
#asset_path: app/build/outputs/apk/release/app-release.apk # 上传路径(Release)
|
|
||||||
asset_path: app/build/outputs/apk/beta/release/app-beta-release.apk # 上传路径(WinBoll Stage Release)
|
|
||||||
asset_name: WinBoll-${{steps.get-version.outputs.result}}0.apk # 资源名
|
|
||||||
asset_content_type: application/vnd.android.package-archiv # 资源类型
|
|
||||||
|
|
||||||
# 存档打包的文件
|
|
||||||
- name: Archive production artifacts
|
|
||||||
uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: build
|
|
||||||
path: app/build/outputs # 将打包之后的文件全部上传(里面会有混淆的 map 文件)
|
|
||||||
@@ -1,166 +1,223 @@
|
|||||||
#!/usr/bin/bash
|
#!/usr/bin/bash
|
||||||
|
# ==============================================================================
|
||||||
|
# WinBoLL 应用发布脚本
|
||||||
|
# 功能:检查Git源码状态 → 编译Stage Release包 → 添加WinBoLL标签 → 提交并推送源码
|
||||||
|
# 依赖:build.properties、app_update_description.txt(项目根目录下)
|
||||||
|
# 使用:./script_name.sh <APP_NAME>
|
||||||
|
# 作者:豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
# 检查是否指定了将要发布的应用名称
|
# ==================== 常量定义 ====================
|
||||||
# 使用 `-z` 命令检查变量是否为空
|
# 脚本退出码
|
||||||
|
EXIT_CODE_SUCCESS=0
|
||||||
|
EXIT_CODE_ERR_NO_APP_NAME=2
|
||||||
|
EXIT_CODE_ERR_WORK_DIR=1
|
||||||
|
EXIT_CODE_ERR_GIT_CHECK=1
|
||||||
|
EXIT_CODE_ERR_ADD_WINBOLL_TAG=1
|
||||||
|
|
||||||
|
# Gradle 任务(正式发布)
|
||||||
|
GRADLE_TASK_PUBLISH="assembleStageRelease"
|
||||||
|
# Gradle 任务(调试用,注释备用)
|
||||||
|
# GRADLE_TASK_DEBUG="assembleBetaDebug"
|
||||||
|
|
||||||
|
# ==================== 函数定义 ====================
|
||||||
|
# 检查Git源码是否已完全提交(无未提交变更)
|
||||||
|
# 返回值:0=已完全提交,1=存在未提交变更
|
||||||
|
function checkGitSources() {
|
||||||
|
# 配置Git安全目录(解决权限问题)
|
||||||
|
git config --global --add safe.directory "$(pwd)"
|
||||||
|
|
||||||
|
# 检查是否有未提交的变更
|
||||||
|
if [[ -n $(git diff --stat) ]]; then
|
||||||
|
echo "[ERROR] Git源码存在未提交变更,请先提交所有修改!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[INFO] Git源码检查通过:所有变更已提交。"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 询问是否添加GitHub Workflows标签(当前逻辑注释,保留扩展能力)
|
||||||
|
# 返回值:1=用户选择是,0=用户选择否
|
||||||
|
function askAddWorkflowsTag() {
|
||||||
|
read -p "是否添加GitHub Workflows标签?(Y/n) " answer
|
||||||
|
if [[ $answer =~ ^[Yy]$ ]]; then
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 添加WinBoLL正式标签
|
||||||
|
# 参数:$1=应用名称(项目根目录名)
|
||||||
|
# 返回值:0=标签添加成功,1=标签已存在/添加失败
|
||||||
|
function addWinBoLLTag() {
|
||||||
|
local app_name=$1
|
||||||
|
local build_prop_path="${app_name}/build.properties"
|
||||||
|
|
||||||
|
# 从build.properties中提取publishVersion
|
||||||
|
local publish_version=$(grep -o "publishVersion=.*" "${build_prop_path}" | awk -F '=' '{print $2}')
|
||||||
|
if [[ -z ${publish_version} ]]; then
|
||||||
|
echo "[ERROR] 未从${build_prop_path}中提取到publishVersion配置!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "[INFO] 从${build_prop_path}读取到publishVersion:${publish_version}"
|
||||||
|
|
||||||
|
# 构造WinBoLL标签(格式:<APP_NAME>-v<publishVersion>)
|
||||||
|
local tag="${app_name}-v${publish_version}"
|
||||||
|
echo "[INFO] 准备添加WinBoLL标签:${tag}"
|
||||||
|
|
||||||
|
# 检查标签是否已存在
|
||||||
|
if [[ "$(git tag -l ${tag})" == "${tag}" ]]; then
|
||||||
|
echo "[ERROR] WinBoLL标签${tag}已存在!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 添加带注释的标签(注释来自app_update_description.txt)
|
||||||
|
git tag -a "${tag}" -F "${app_name}/app_update_description.txt"
|
||||||
|
echo "[INFO] WinBoLL标签${tag}添加成功!"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 添加GitHub Workflows Beta标签(当前逻辑注释,保留扩展能力)
|
||||||
|
# 参数:$1=应用名称(项目根目录名)
|
||||||
|
# 返回值:0=标签添加成功,1=标签已存在/添加失败
|
||||||
|
function addWorkflowsTag() {
|
||||||
|
local app_name=$1
|
||||||
|
local build_prop_path="${app_name}/build.properties"
|
||||||
|
|
||||||
|
# 从build.properties中提取baseBetaVersion
|
||||||
|
local base_beta_version=$(grep -o "baseBetaVersion=.*" "${build_prop_path}" | awk -F '=' '{print $2}')
|
||||||
|
if [[ -z ${base_beta_version} ]]; then
|
||||||
|
echo "[ERROR] 未从${build_prop_path}中提取到baseBetaVersion配置!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "[INFO] 从${build_prop_path}读取到baseBetaVersion:${base_beta_version}"
|
||||||
|
|
||||||
|
# 构造Workflows标签(格式:<APP_NAME>-v<baseBetaVersion>-beta)
|
||||||
|
local tag="${app_name}-v${base_beta_version}-beta"
|
||||||
|
echo "[INFO] 准备添加Workflows标签:${tag}"
|
||||||
|
|
||||||
|
# 检查标签是否已存在
|
||||||
|
if [[ "$(git tag -l ${tag})" == "${tag}" ]]; then
|
||||||
|
echo "[ERROR] Workflows标签${tag}已存在!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 添加带注释的标签(注释来自app_update_description.txt)
|
||||||
|
git tag -a "${tag}" -F "${app_name}/app_update_description.txt"
|
||||||
|
echo "[INFO] Workflows标签${tag}添加成功!"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================== 主流程开始 ====================
|
||||||
|
echo "============================================="
|
||||||
|
echo " WinBoLL 应用发布脚本"
|
||||||
|
echo "============================================="
|
||||||
|
|
||||||
|
# 1. 检查应用名称参数是否指定
|
||||||
if [ -z "$1" ]; then
|
if [ -z "$1" ]; then
|
||||||
echo "No APP name specified : $0"
|
echo "[ERROR] 未指定应用名称!使用方式:${0} <APP_NAME>"
|
||||||
exit 2
|
exit ${EXIT_CODE_ERR_NO_APP_NAME}
|
||||||
fi
|
fi
|
||||||
|
APP_NAME=$1
|
||||||
|
echo "[INFO] 待发布应用名称:${APP_NAME}"
|
||||||
|
|
||||||
## 定义相关函数
|
# 2. 检查并切换到项目根目录(确保build.properties存在)
|
||||||
## 检查 Git 源码是否完全提交了,完全提交就返回0
|
echo "[INFO] 当前工作目录:$(pwd)"
|
||||||
function checkGitSources {
|
if [[ ! -e "${APP_NAME}/build.properties" ]]; then
|
||||||
#local input="$1"
|
echo "[WARNING] 当前目录不存在${APP_NAME}/build.properties,尝试切换到上级目录..."
|
||||||
#echo "The string is: $input"
|
|
||||||
git config --global --add safe.directory `pwd`
|
|
||||||
if [[ -n $(git diff --stat) ]]
|
|
||||||
then
|
|
||||||
local result="Source is no commit completely."
|
|
||||||
echo $result
|
|
||||||
# 脚本调试时使用
|
|
||||||
#return 0
|
|
||||||
# 正式检查源码时使用
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
local result="Git Source Check OK."
|
|
||||||
echo $result
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function askAddWorkflowsTag {
|
|
||||||
read answer
|
|
||||||
if [[ $answer =~ ^[Yy]$ ]]; then
|
|
||||||
#echo "You chose yes."
|
|
||||||
return 1
|
|
||||||
else
|
|
||||||
#echo "You chose no."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function addWinBoLLTag {
|
|
||||||
# 就读取脚本 .winboll/winboll_app_build.gradle 生成的 publishVersion。
|
|
||||||
# 如果文件中有 publishVersion 这一项,
|
|
||||||
# 使用grep找到包含"publishVersion="的那一行,然后用awk提取其后的值
|
|
||||||
PUBLISH_VERSION=$(grep -o "publishVersion=.*" $1/build.properties | awk -F '=' '{print $2}')
|
|
||||||
echo "< $1/build.properties publishVersion : ${PUBLISH_VERSION} >"
|
|
||||||
## 设新的 WinBoLL 标签
|
|
||||||
# 脚本调试时使用
|
|
||||||
#tag="projectname-v7.6.4-test1"
|
|
||||||
# 正式设置标签时使用
|
|
||||||
tag=$1"-v"${PUBLISH_VERSION}
|
|
||||||
echo "< WinBoLL Tag To: $tag >";
|
|
||||||
# 检查是否已经添加了 WinBoLL Tag
|
|
||||||
if [ "$(git tag -l ${tag})" == "${tag}" ]; then
|
|
||||||
echo -e "< WinBoLL Tag ${tag} exist! >"
|
|
||||||
return 1 # WinBoLL标签重复
|
|
||||||
fi
|
|
||||||
# 添加WinBoLL标签
|
|
||||||
git tag -a ${tag} -F $1/app_update_description.txt
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function addWorkflowsTag {
|
|
||||||
# 就读取脚本 .winboll/winboll_app_build.gradle 生成的 baseBetaVersion。
|
|
||||||
# 如果文件中有 baseBetaVersion 这一项,
|
|
||||||
# 使用grep找到包含"baseBetaVersion="的那一行,然后用awk提取其后的值
|
|
||||||
BASE_BETA_VERSION=$(grep -o "baseBetaVersion=.*" $1/build.properties | awk -F '=' '{print $2}')
|
|
||||||
echo "< $1/build.properties baseBetaVersion : ${BASE_BETA_VERSION} >"
|
|
||||||
## 设新的 workflows 标签
|
|
||||||
# 脚本调试时使用
|
|
||||||
#tag="projectname-v7.6.4-beta"
|
|
||||||
# 正式设置标签时使用
|
|
||||||
tag=$1"-v"${BASE_BETA_VERSION}-beta
|
|
||||||
echo "< Workflows Tag To: $tag >";
|
|
||||||
# 检查是否已经添加了工作流 Tag
|
|
||||||
if [ "$(git tag -l ${tag})" == "${tag}" ]; then
|
|
||||||
echo -e "< Github Workflows Tag ${tag} exist! >"
|
|
||||||
return 1 # 工作流标签重复
|
|
||||||
fi
|
|
||||||
# 添加工作流标签
|
|
||||||
git tag -a ${tag} -F $1/app_update_description.txt
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
## 开始执行脚本
|
|
||||||
echo -e "Current dir : \n"`pwd`
|
|
||||||
# 检查当前目录是否是项目根目录
|
|
||||||
if [[ -e $1/build.properties ]]; then
|
|
||||||
echo "The $1/build.properties file exists."
|
|
||||||
echo -e "Work dir correctly."
|
|
||||||
else
|
|
||||||
echo "The $1/build.properties file does not exist."
|
|
||||||
echo "尝试进入根目录"
|
|
||||||
# 进入项目根目录
|
|
||||||
cd ..
|
cd ..
|
||||||
|
echo "[INFO] 切换后工作目录:$(pwd)"
|
||||||
fi
|
fi
|
||||||
## 本脚本需要在项目根目录下执行
|
|
||||||
echo -e "Current dir : \n"`pwd`
|
# 验证最终工作目录是否正确
|
||||||
# 检查当前目录是否是项目根目录
|
if [[ ! -e "${APP_NAME}/build.properties" ]]; then
|
||||||
if [[ -e $1/build.properties ]]; then
|
echo "[ERROR] 工作目录错误!${APP_NAME}/build.properties 文件不存在。"
|
||||||
echo "The $1/build.properties file exists."
|
exit ${EXIT_CODE_ERR_WORK_DIR}
|
||||||
echo -e "Work dir correctly."
|
fi
|
||||||
|
echo "[INFO] 工作目录验证通过:${APP_NAME}/build.properties 存在。"
|
||||||
|
|
||||||
|
# 3. 检查Git源码状态
|
||||||
|
echo "---------------------------------------------"
|
||||||
|
echo " 步骤1:检查Git源码状态"
|
||||||
|
echo "---------------------------------------------"
|
||||||
|
checkGitSources
|
||||||
|
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
|
||||||
|
echo "[ERROR] Git源码检查失败,脚本终止!"
|
||||||
|
exit ${EXIT_CODE_ERR_GIT_CHECK}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. 编译Stage Release版本APK
|
||||||
|
echo "---------------------------------------------"
|
||||||
|
echo " 步骤2:编译Stage Release APK"
|
||||||
|
echo "---------------------------------------------"
|
||||||
|
echo "[INFO] 开始执行Gradle任务:${GRADLE_TASK_PUBLISH}"
|
||||||
|
# 调试用(注释正式任务,启用调试任务)
|
||||||
|
# bash gradlew :${APP_NAME}:${GRADLE_TASK_DEBUG}
|
||||||
|
bash gradlew :${APP_NAME}:${GRADLE_TASK_PUBLISH}
|
||||||
|
|
||||||
|
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
|
||||||
|
echo "[ERROR] Gradle编译任务失败!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[INFO] Stage Release APK编译成功!"
|
||||||
|
|
||||||
|
# 5. 添加WinBoLL正式标签
|
||||||
|
echo "---------------------------------------------"
|
||||||
|
echo " 步骤3:添加WinBoLL标签"
|
||||||
|
echo "---------------------------------------------"
|
||||||
|
addWinBoLLTag ${APP_NAME}
|
||||||
|
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
|
||||||
|
echo "[ERROR] WinBoLL标签添加失败,脚本终止!"
|
||||||
|
exit ${EXIT_CODE_ERR_ADD_WINBOLL_TAG}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6. (可选)添加GitHub Workflows标签(当前逻辑注释,保留扩展能力)
|
||||||
|
# echo "---------------------------------------------"
|
||||||
|
# echo " 步骤4:添加Workflows标签(可选)"
|
||||||
|
# echo "---------------------------------------------"
|
||||||
|
# echo "是否添加GitHub Workflows Beta标签?(Y/n) "
|
||||||
|
# askAddWorkflowsTag
|
||||||
|
# nAskAddWorkflowsTag=$?
|
||||||
|
# if [[ ${nAskAddWorkflowsTag} -eq 1 ]]; then
|
||||||
|
# addWorkflowsTag ${APP_NAME}
|
||||||
|
# if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
|
||||||
|
# echo "[ERROR] Workflows标签添加失败,脚本终止!"
|
||||||
|
# exit 1
|
||||||
|
# fi
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# 7. 清理更新描述文件
|
||||||
|
echo "---------------------------------------------"
|
||||||
|
echo " 步骤5:清理更新描述文件"
|
||||||
|
echo "---------------------------------------------"
|
||||||
|
echo "" > "${APP_NAME}/app_update_description.txt"
|
||||||
|
echo "[INFO] 已清空${APP_NAME}/app_update_description.txt"
|
||||||
|
|
||||||
|
# 8. 提交并推送源码与标签
|
||||||
|
echo "---------------------------------------------"
|
||||||
|
echo " 步骤6:提交并推送源码"
|
||||||
|
echo "---------------------------------------------"
|
||||||
|
git add .
|
||||||
|
git commit -m "<${APP_NAME}> 开始新的Stage版本开发。"
|
||||||
|
echo "[INFO] 源码提交成功,开始推送..."
|
||||||
|
|
||||||
|
# 推送源码到远程仓库
|
||||||
|
git push origin
|
||||||
|
# 推送标签到远程仓库
|
||||||
|
git push origin --tags
|
||||||
|
|
||||||
|
if [[ $? -eq ${EXIT_CODE_SUCCESS} ]]; then
|
||||||
|
echo "[INFO] 源码与标签推送成功!"
|
||||||
else
|
else
|
||||||
echo "The $1/build.properties file does not exist."
|
echo "[ERROR] 源码与标签推送失败!"
|
||||||
echo -e "Work dir error."
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检查源码状态
|
# ==================== 主流程结束 ====================
|
||||||
result=$(checkGitSources)
|
echo "============================================="
|
||||||
if [[ $? -eq 0 ]]; then
|
echo " WinBoLL 应用发布完成!"
|
||||||
echo $result
|
echo "============================================="
|
||||||
# 如果Git已经提交了所有代码就执行标签和应用发布操作
|
exit ${EXIT_CODE_SUCCESS}
|
||||||
|
|
||||||
# 预先询问是否添加工作流标签
|
|
||||||
#echo "Add Github Workflows Tag? (yes/No)"
|
|
||||||
#result=$(askAddWorkflowsTag)
|
|
||||||
#nAskAddWorkflowsTag=$?
|
|
||||||
#echo $result
|
|
||||||
|
|
||||||
# 发布应用
|
|
||||||
echo "Publishing WinBoLL APK ..."
|
|
||||||
# 脚本调试时使用
|
|
||||||
#bash gradlew :$1:assembleBetaDebug
|
|
||||||
# 正式发布
|
|
||||||
bash gradlew :$1:assembleStageRelease
|
|
||||||
echo "Publishing WinBoLL APK OK."
|
|
||||||
|
|
||||||
# 添加 WinBoLL 标签
|
|
||||||
result=$(addWinBoLLTag $1)
|
|
||||||
echo $result
|
|
||||||
if [[ $? -eq 0 ]]; then
|
|
||||||
echo $result
|
|
||||||
# WinBoLL 标签添加成功
|
|
||||||
else
|
|
||||||
echo -e "${0}: addWinBoLLTag $1\n${result}\nAdd WinBoLL tag cancel."
|
|
||||||
exit 1 # addWinBoLLTag 异常
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 添加 GitHub 工作流标签
|
|
||||||
#if [[ $nAskAddWorkflowsTag -eq 1 ]]; then
|
|
||||||
# 如果用户选择添加工作流标签
|
|
||||||
#result=$(addWorkflowsTag $1)
|
|
||||||
#if [[ $? -eq 0 ]]; then
|
|
||||||
# echo $result
|
|
||||||
# 工作流标签添加成功
|
|
||||||
#else
|
|
||||||
#echo -e "${0}: addWorkflowsTag $1\n${result}\nAdd workflows tag cancel."
|
|
||||||
#exit 1 # addWorkflowsTag 异常
|
|
||||||
#fi
|
|
||||||
#fi
|
|
||||||
|
|
||||||
## 清理更新描述文件内容
|
|
||||||
echo "" > $1/app_update_description.txt
|
|
||||||
|
|
||||||
# 设置新版本开发参数配置
|
|
||||||
# 提交配置
|
|
||||||
git add .
|
|
||||||
git commit -m "<$1>Start New Stage Version."
|
|
||||||
echo "Push sources to git repositories ..."
|
|
||||||
# 推送源码到所有仓库
|
|
||||||
git push origin && git push origin --tags
|
|
||||||
else
|
|
||||||
echo -e "${0}: checkGitSources\n${result}\nShell cancel."
|
|
||||||
exit 1 # checkGitSources 异常
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -65,6 +65,11 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_11
|
||||||
|
targetCompatibility JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
|
||||||
// 应用包输出配置
|
// 应用包输出配置
|
||||||
//
|
//
|
||||||
android.applicationVariants.all { variant ->
|
android.applicationVariants.all { variant ->
|
||||||
|
|||||||
@@ -155,3 +155,11 @@ $ bash gradlew assembleBetaDebug
|
|||||||
$ bash gradlew assembleStageDebug
|
$ bash gradlew assembleStageDebug
|
||||||
|
|
||||||
### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。
|
### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。
|
||||||
|
|
||||||
|
# 应用版本号命名方式
|
||||||
|
## statge 渠道
|
||||||
|
V<应用开发环境编号><应用功能变更号><应用调试阶段号>
|
||||||
|
如:APPBase_15.7.0
|
||||||
|
## beta 渠道
|
||||||
|
V<应用开发环境编号><应用功能变更号><应用调试阶段号>-beta<调试编译计数>_<调试编译时间(分钟+秒钟)>
|
||||||
|
如:APPBase_15.9.6-beta8_5413
|
||||||
|
|||||||
@@ -100,12 +100,15 @@ allprojects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
|
// 1. 对纯 Java 模块的 JavaCompile 任务配置(升级为 Java 11)
|
||||||
tasks.withType(JavaCompile) {
|
tasks.withType(JavaCompile) {
|
||||||
options.compilerArgs << "-parameters"
|
options.compilerArgs << "-parameters"
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
// 可选:确保编码一致
|
||||||
|
options.encoding = "UTF-8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task clean(type: Delete) {
|
task clean(type: Delete) {
|
||||||
|
|||||||
@@ -18,12 +18,16 @@ def genVersionName(def versionName){
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 30
|
|
||||||
buildToolsVersion "30.0.3"
|
// 关键:改为你已安装的 SDK 32(≥ targetSdkVersion 30,兼容已安装环境)
|
||||||
|
compileSdkVersion 32
|
||||||
|
|
||||||
|
// 直接使用已安装的构建工具 33.0.3(无需修改)
|
||||||
|
buildToolsVersion "33.0.3"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "cc.winboll.studio.contacts"
|
applicationId "cc.winboll.studio.contacts"
|
||||||
minSdkVersion 24
|
minSdkVersion 23
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode 2
|
versionCode 2
|
||||||
// versionName 更新后需要手动设置
|
// versionName 更新后需要手动设置
|
||||||
@@ -86,14 +90,13 @@ dependencies {
|
|||||||
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
|
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
|
||||||
//api 'androidx.fragment:fragment:1.1.0'
|
//api 'androidx.fragment:fragment:1.1.0'
|
||||||
|
|
||||||
|
|
||||||
// WinBoLL库 nexus.winboll.cc 地址
|
// WinBoLL库 nexus.winboll.cc 地址
|
||||||
//api 'cc.winboll.studio:libaes:15.12.0'
|
api 'cc.winboll.studio:libaes:15.12.12'
|
||||||
//api 'cc.winboll.studio:libappbase:15.12.2'
|
api 'cc.winboll.studio:libappbase:15.14.2'
|
||||||
|
|
||||||
// WinBoLL备用库 jitpack.io 地址
|
// WinBoLL备用库 jitpack.io 地址
|
||||||
api 'com.github.ZhanGSKen:AES:aes-v15.12.3'
|
//api 'com.github.ZhanGSKen:AES:aes-v15.12.9'
|
||||||
api 'com.github.ZhanGSKen:APPBase:appbase-v15.12.2'
|
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1'
|
||||||
|
|
||||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
#Sat Dec 13 15:30:16 HKT 2025
|
#Tue Jan 06 20:25:41 HKT 2026
|
||||||
stageCount=1
|
stageCount=3
|
||||||
libraryProject=
|
libraryProject=
|
||||||
baseVersion=15.14
|
baseVersion=15.14
|
||||||
publishVersion=15.14.0
|
publishVersion=15.14.2
|
||||||
buildCount=0
|
buildCount=0
|
||||||
baseBetaVersion=15.14.1
|
baseBetaVersion=15.14.3
|
||||||
|
|||||||
@@ -1,139 +1,313 @@
|
|||||||
package cc.winboll.studio.contacts;
|
package cc.winboll.studio.contacts;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||||
* @Date 2025/02/13 06:58:04
|
* @Date 2025/02/13 06:58:04
|
||||||
* @Describe Activity 栈管理工具,用于统一管理应用内 Activity 生命周期
|
* @Describe Activity 栈管理工具,统一管理应用内 Activity 生命周期
|
||||||
|
* 适配:Java7 + Android API29-30 + 小米机型,优化并发安全与通话场景稳定性
|
||||||
*/
|
*/
|
||||||
public class ActivityStack {
|
public class ActivityStack {
|
||||||
// ====================== 常量定义区 ======================
|
// 常量定义(核心标识+版本兼容常量)
|
||||||
public static final String TAG = "ActivityStack";
|
public static final String TAG = "ActivityStack";
|
||||||
|
private static final int API_VERSION_O = 26; // Android 8.0 API26(isDestroyed适配用)
|
||||||
|
|
||||||
// ====================== 单例与成员变量区 ======================
|
// 单例与核心成员变量(按优先级排序)
|
||||||
private static final ActivityStack INSTANCE = new ActivityStack();
|
private static final ActivityStack INSTANCE = new ActivityStack();
|
||||||
private List<Activity> mActivityList = new ArrayList<Activity>();
|
// 替换为ArrayList+同步锁:解决CopyOnWriteArrayList迭代器不能删除的崩溃,兼顾并发安全
|
||||||
|
private final List<Activity> mActivityList = new ArrayList<Activity>();
|
||||||
|
private final Handler mMainHandler = new Handler(Looper.getMainLooper()); // 复用主线程Handler,避免内存泄漏
|
||||||
|
|
||||||
// ====================== 单例获取方法区 ======================
|
// 单例对外暴露方法
|
||||||
public static ActivityStack getInstance() {
|
public static ActivityStack getInstance() {
|
||||||
return INSTANCE;
|
return INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================== 私有构造函数(防止外部实例化) ======================
|
// 私有构造,禁止外部实例化
|
||||||
private ActivityStack() {
|
private ActivityStack() {
|
||||||
LogUtils.d(TAG, "ActivityStack: 初始化 Activity 栈管理工具");
|
LogUtils.d(TAG, "ActivityStack 初始化完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================== Activity 栈操作方法区 ======================
|
// ====================== 栈基础操作(添加/移除) ======================
|
||||||
/**
|
/**
|
||||||
* 添加 Activity 到栈中
|
* 添加Activity到栈中,避免重复入栈
|
||||||
|
* @param activity 待添加的Activity
|
||||||
*/
|
*/
|
||||||
public void addActivity(Activity activity) {
|
public void addActivity(Activity activity) {
|
||||||
if (activity == null) {
|
if (activity == null) {
|
||||||
LogUtils.w(TAG, "addActivity: 待添加的 Activity 为 null,跳过添加");
|
LogUtils.w(TAG, "addActivity: activity is null, skip");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mActivityList.add(activity);
|
// 同步锁:解决多线程并发添加冲突(小米机型多线程场景适配)
|
||||||
LogUtils.d(TAG, "addActivity: Activity入栈 | 类名=" + activity.getClass().getSimpleName() + " | 栈大小=" + mActivityList.size());
|
synchronized (mActivityList) {
|
||||||
}
|
if (!mActivityList.contains(activity)) {
|
||||||
|
mActivityList.add(activity);
|
||||||
/**
|
LogUtils.d(TAG, "addActivity: " + activity.getClass().getSimpleName() + ", stack size: " + mActivityList.size());
|
||||||
* 获取栈顶 Activity
|
|
||||||
*/
|
|
||||||
public Activity getTopActivity() {
|
|
||||||
if (mActivityList.isEmpty()) {
|
|
||||||
LogUtils.w(TAG, "getTopActivity: Activity 栈为空,返回 null");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Activity topActivity = mActivityList.get(mActivityList.size() - 1);
|
|
||||||
LogUtils.d(TAG, "getTopActivity: 获取栈顶 Activity | 类名=" + topActivity.getClass().getSimpleName());
|
|
||||||
return topActivity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除并销毁栈顶 Activity
|
|
||||||
*/
|
|
||||||
public void finishTopActivity() {
|
|
||||||
if (mActivityList.isEmpty()) {
|
|
||||||
LogUtils.w(TAG, "finishTopActivity: Activity 栈为空,无需操作");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Activity topActivity = mActivityList.remove(mActivityList.size() - 1);
|
|
||||||
topActivity.finish();
|
|
||||||
LogUtils.d(TAG, "finishTopActivity: 销毁栈顶 Activity | 类名=" + topActivity.getClass().getSimpleName() + " | 栈大小=" + mActivityList.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除并销毁指定 Activity
|
|
||||||
*/
|
|
||||||
public void finishActivity(Activity activity) {
|
|
||||||
if (activity == null) {
|
|
||||||
LogUtils.w(TAG, "finishActivity: 待销毁的 Activity 为 null,跳过操作");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (mActivityList.remove(activity)) {
|
|
||||||
activity.finish();
|
|
||||||
LogUtils.d(TAG, "finishActivity: 销毁指定 Activity | 类名=" + activity.getClass().getSimpleName() + " | 栈大小=" + mActivityList.size());
|
|
||||||
} else {
|
|
||||||
LogUtils.w(TAG, "finishActivity: 指定 Activity 不在栈中 | 类名=" + activity.getClass().getSimpleName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除并销毁指定类的所有 Activity
|
|
||||||
*/
|
|
||||||
public void finishActivity(Class<?> activityClass) {
|
|
||||||
if (activityClass == null) {
|
|
||||||
LogUtils.w(TAG, "finishActivity: 待销毁的 Activity 类为 null,跳过操作");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Java7 兼容:使用 Iterator 遍历避免 ConcurrentModificationException
|
|
||||||
Iterator<Activity> iterator = mActivityList.iterator();
|
|
||||||
while (iterator.hasNext()) {
|
|
||||||
Activity activity = iterator.next();
|
|
||||||
if (activity.getClass().equals(activityClass)) {
|
|
||||||
iterator.remove();
|
|
||||||
activity.finish();
|
|
||||||
LogUtils.d(TAG, "finishActivity: 销毁指定类 Activity | 类名=" + activityClass.getSimpleName() + " | 栈大小=" + mActivityList.size());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 销毁栈中所有 Activity
|
* 移除Activity(不销毁,用于正常退出场景)
|
||||||
*/
|
* @param activity 待移除的Activity
|
||||||
public void finishAllActivity() {
|
|
||||||
if (mActivityList.isEmpty()) {
|
|
||||||
LogUtils.w(TAG, "finishAllActivity: Activity 栈为空,无需操作");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Java7 兼容:使用增强 for 循环遍历销毁,避免迭代器异常
|
|
||||||
for (Activity activity : mActivityList) {
|
|
||||||
if (!activity.isFinishing()) {
|
|
||||||
activity.finish();
|
|
||||||
LogUtils.d(TAG, "finishAllActivity: 销毁 Activity | 类名=" + activity.getClass().getSimpleName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mActivityList.clear();
|
|
||||||
LogUtils.d(TAG, "finishAllActivity: 所有 Activity 已销毁,栈已清空");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 新增:移除指定Activity但不销毁(用于Activity正常退出)
|
|
||||||
*/
|
*/
|
||||||
public void removeActivity(Activity activity) {
|
public void removeActivity(Activity activity) {
|
||||||
if (activity == null) {
|
if (activity == null) {
|
||||||
LogUtils.w(TAG, "removeActivity: 待移除的 Activity 为 null,跳过操作");
|
LogUtils.w(TAG, "removeActivity: activity is null, skip");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mActivityList.remove(activity)) {
|
synchronized (mActivityList) {
|
||||||
LogUtils.d(TAG, "removeActivity: 移除 Activity | 类名=" + activity.getClass().getSimpleName() + " | 栈大小=" + mActivityList.size());
|
if (mActivityList.remove(activity)) {
|
||||||
|
LogUtils.d(TAG, "removeActivity: " + activity.getClass().getSimpleName() + ", stack size: " + mActivityList.size());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ====================== Activity状态查询(获取/判断存活) ======================
|
||||||
|
/**
|
||||||
|
* 获取栈顶有效Activity(迭代遍历替代递归,避免栈溢出,适配小米多页面场景)
|
||||||
|
* @return 栈顶有效Activity,无则返回null
|
||||||
|
*/
|
||||||
|
public Activity getTopActivity() {
|
||||||
|
synchronized (mActivityList) {
|
||||||
|
if (mActivityList.isEmpty()) {
|
||||||
|
LogUtils.w(TAG, "getTopActivity: stack is empty, return null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Activity validTopActivity = null;
|
||||||
|
// 倒序遍历,优先取最顶层有效Activity,同时清理无效残留
|
||||||
|
for (int i = mActivityList.size() - 1; i >= 0; i--) {
|
||||||
|
Activity activity = mActivityList.get(i);
|
||||||
|
// 版本兼容校验:API26+才支持isDestroyed
|
||||||
|
if (activity != null && !activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
|
||||||
|
validTopActivity = activity;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
mActivityList.remove(i);
|
||||||
|
String className = (activity != null) ? activity.getClass().getSimpleName() : "null";
|
||||||
|
LogUtils.w(TAG, "getTopActivity: remove invalid activity: " + className);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validTopActivity != null) {
|
||||||
|
LogUtils.d(TAG, "getTopActivity: top activity: " + validTopActivity.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
return validTopActivity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定类的有效Activity实例(通话场景核心方法,判断页面是否存活)
|
||||||
|
* @param activityClass 目标Activity类
|
||||||
|
* @return 有效实例,无则返回null
|
||||||
|
*/
|
||||||
|
public Activity getActivity(Class<?> activityClass) {
|
||||||
|
if (activityClass == null) {
|
||||||
|
LogUtils.w(TAG, "getActivity: activityClass is null, return null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
synchronized (mActivityList) {
|
||||||
|
if (mActivityList.isEmpty()) {
|
||||||
|
LogUtils.w(TAG, "getActivity: stack empty, return null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Activity activity : mActivityList) {
|
||||||
|
if (activity != null && activity.getClass().equals(activityClass) && !activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
|
||||||
|
LogUtils.d(TAG, "getActivity: find valid activity: " + activityClass.getSimpleName());
|
||||||
|
return activity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogUtils.w(TAG, "getActivity: no valid activity: " + activityClass.getSimpleName());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断指定Activity是否存活(简化通话场景调用,避免重复判空)
|
||||||
|
* @param activityClass 目标Activity类
|
||||||
|
* @return true:存活,false:未存活
|
||||||
|
*/
|
||||||
|
public boolean isActivityAlive(Class<?> activityClass) {
|
||||||
|
boolean isAlive = getActivity(activityClass) != null;
|
||||||
|
LogUtils.d(TAG, "isActivityAlive: " + activityClass.getSimpleName() + ", result: " + isAlive);
|
||||||
|
return isAlive;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== Activity销毁操作(单/批量/全部) ======================
|
||||||
|
/**
|
||||||
|
* 销毁栈顶Activity(主线程执行,适配小米机型线程限制)
|
||||||
|
*/
|
||||||
|
public void finishTopActivity() {
|
||||||
|
runOnMainThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
synchronized (mActivityList) {
|
||||||
|
if (mActivityList.isEmpty()) {
|
||||||
|
LogUtils.w(TAG, "finishTopActivity: stack is empty, skip");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先移除再校验,避免并发冲突(小米多线程场景适配)
|
||||||
|
Activity topActivity = mActivityList.remove(mActivityList.size() - 1);
|
||||||
|
if (topActivity == null) {
|
||||||
|
LogUtils.w(TAG, "finishTopActivity: top activity is null, skip");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!topActivity.isFinishing() && (getSdkVersion() < API_VERSION_O || !topActivity.isDestroyed())) {
|
||||||
|
topActivity.finish();
|
||||||
|
LogUtils.d(TAG, "finishTopActivity: destroy top activity: " + topActivity.getClass().getSimpleName() + ", stack size: " + mActivityList.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁指定Activity(主线程执行,避免跨线程异常)
|
||||||
|
* @param activity 待销毁的Activity
|
||||||
|
*/
|
||||||
|
public void finishActivity(final Activity activity) {
|
||||||
|
runOnMainThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (activity == null) {
|
||||||
|
LogUtils.w(TAG, "finishActivity: activity is null, skip");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (mActivityList) {
|
||||||
|
if (mActivityList.contains(activity) && !activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
|
||||||
|
mActivityList.remove(activity);
|
||||||
|
activity.finish();
|
||||||
|
LogUtils.d(TAG, "finishActivity: destroy activity: " + activity.getClass().getSimpleName() + ", stack size: " + mActivityList.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁指定类的所有Activity(核心修复:迭代器删除崩溃,通话场景核心)
|
||||||
|
* @param activityClass 目标Activity类
|
||||||
|
*/
|
||||||
|
public void finishActivity(final Class<?> activityClass) {
|
||||||
|
runOnMainThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (activityClass == null) {
|
||||||
|
LogUtils.w(TAG, "finishActivity: activityClass is null, skip");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (mActivityList) {
|
||||||
|
if (mActivityList.isEmpty()) {
|
||||||
|
LogUtils.w(TAG, "finishActivity: stack empty, skip");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 核心修复:用索引遍历+倒序删除,替代迭代器删除(避免UnsupportedOperationException)
|
||||||
|
for (int i = mActivityList.size() - 1; i >= 0; i--) {
|
||||||
|
Activity activity = mActivityList.get(i);
|
||||||
|
if (activity != null && activity.getClass().equals(activityClass)) {
|
||||||
|
if (!activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
|
||||||
|
mActivityList.remove(i); // 索引删除,支持ArrayList
|
||||||
|
activity.finish();
|
||||||
|
LogUtils.d(TAG, "finishActivity: destroy class activity: " + activityClass.getSimpleName() + ", stack size: " + mActivityList.size());
|
||||||
|
} else {
|
||||||
|
mActivityList.remove(i); // 清理无效残留
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁栈中所有Activity(退出应用/清空栈场景用)
|
||||||
|
*/
|
||||||
|
public void finishAllActivity() {
|
||||||
|
runOnMainThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
synchronized (mActivityList) {
|
||||||
|
if (mActivityList.isEmpty()) {
|
||||||
|
LogUtils.w(TAG, "finishAllActivity: stack is empty, skip");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历销毁所有有效Activity,逐个状态校验(小米机型稳定性适配)
|
||||||
|
for (Activity activity : mActivityList) {
|
||||||
|
if (activity != null && !activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
|
||||||
|
activity.finish();
|
||||||
|
LogUtils.d(TAG, "finishAllActivity: destroy activity: " + activity.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mActivityList.clear();
|
||||||
|
LogUtils.d(TAG, "finishAllActivity: all activity destroyed, stack cleared");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 栈优化与工具方法 ======================
|
||||||
|
/**
|
||||||
|
* 清理栈中所有无效Activity(null/已销毁/已结束),优化小米机型内存占用
|
||||||
|
*/
|
||||||
|
public void clearInvalidActivities() {
|
||||||
|
runOnMainThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
synchronized (mActivityList) {
|
||||||
|
if (mActivityList.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 倒序索引删除,避免遍历过程中索引错乱
|
||||||
|
for (int i = mActivityList.size() - 1; i >= 0; i--) {
|
||||||
|
Activity activity = mActivityList.get(i);
|
||||||
|
if (activity == null || activity.isFinishing() || (getSdkVersion() >= API_VERSION_O && activity.isDestroyed())) {
|
||||||
|
mActivityList.remove(i);
|
||||||
|
String className = (activity != null) ? activity.getClass().getSimpleName() : "null";
|
||||||
|
LogUtils.d(TAG, "clearInvalidActivities: remove invalid activity: " + className);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogUtils.d(TAG, "clearInvalidActivities: done, stack size: " + mActivityList.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保任务在主线程执行(Activity操作必须主线程,小米机型严格限制)
|
||||||
|
* @param runnable 待执行任务
|
||||||
|
*/
|
||||||
|
private void runOnMainThread(Runnable runnable) {
|
||||||
|
if (runnable == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 避免不必要的线程切换,优化性能(小米机型流畅度适配)
|
||||||
|
if (Looper.getMainLooper() == Looper.myLooper()) {
|
||||||
|
runnable.run();
|
||||||
|
} else {
|
||||||
|
mMainHandler.post(runnable);
|
||||||
|
LogUtils.d(TAG, "runOnMainThread: post task to main thread");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 辅助方法:获取当前系统SDK版本(简化版本判断逻辑,统一调用)
|
||||||
|
* @return SDK版本号
|
||||||
|
*/
|
||||||
|
private int getSdkVersion() {
|
||||||
|
return android.os.Build.VERSION.SDK_INT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,161 +1,362 @@
|
|||||||
package cc.winboll.studio.contacts.phonecallui;
|
package cc.winboll.studio.contacts.phonecallui;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.Message;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.Window;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import androidx.annotation.RequiresApi;
|
import android.widget.Toast;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import cc.winboll.studio.contacts.ActivityStack;
|
import cc.winboll.studio.contacts.ActivityStack;
|
||||||
import cc.winboll.studio.contacts.MainActivity;
|
|
||||||
import cc.winboll.studio.contacts.R;
|
import cc.winboll.studio.contacts.R;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
import java.util.Timer;
|
import java.util.Timer;
|
||||||
import java.util.TimerTask;
|
import java.util.TimerTask;
|
||||||
|
|
||||||
import static cc.winboll.studio.contacts.listenphonecall.CallListenerService.formatPhoneNumber;
|
import static cc.winboll.studio.contacts.listenphonecall.CallListenerService.formatPhoneNumber;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提供接打电话的界面,仅支持 Android M (6.0, API 23) 及以上的系统
|
* @Author aJIEw, ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||||
*
|
* @Date 2025/12/14 21:01
|
||||||
* @author aJIEw
|
* @Describe 接打电话界面(单例模式 + 适配API29 - 30 + 小米机型兼容性优化)
|
||||||
|
* 功能:单例通话窗口、来电/去电显示、通话计时、免提控制、锁屏显示
|
||||||
*/
|
*/
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
public class PhoneCallActivity extends Activity implements View.OnClickListener {
|
||||||
public class PhoneCallActivity extends AppCompatActivity implements View.OnClickListener {
|
// 常量定义区(核心常量+小米适配标识)
|
||||||
|
public static final String TAG = "PhoneCallActivity";
|
||||||
|
private static final int MSG_CLOSE_ACTIVITY = 0x001;
|
||||||
|
private static final String MI_ADAPT_TAG = "MiAdapt";
|
||||||
|
private static final String TOAST_CALLING = "通话进行中,无法重复创建通话窗口";
|
||||||
|
private static final long CLOSE_DELAY_MS = 100; // 小米机型关闭延迟时间
|
||||||
|
|
||||||
private TextView tvCallNumberLabel;
|
// 静态属性区(单例核心+全局工具对象)
|
||||||
private TextView tvCallNumber;
|
private static volatile boolean sIsActivityAlive = false;
|
||||||
private TextView tvPickUp;
|
private static Handler sCloseHandler;
|
||||||
private TextView tvCallingTime;
|
|
||||||
private TextView tvHangUp;
|
|
||||||
|
|
||||||
private PhoneCallManager phoneCallManager;
|
// 控件属性区(按界面布局顺序排列)
|
||||||
private PhoneCallService.CallType callType;
|
private TextView mTvCallNumberLabel;
|
||||||
private String phoneNumber;
|
private TextView mTvCallNumber;
|
||||||
|
private TextView mTvPickUp;
|
||||||
|
private TextView mTvCallingTime;
|
||||||
|
private TextView mTvHangUp;
|
||||||
|
|
||||||
private Timer onGoingCallTimer;
|
// 业务属性区(按依赖优先级排列)
|
||||||
private int callingTime;
|
private PhoneCallManager mPhoneCallManager;
|
||||||
|
private PhoneCallService.CallType mCallType;
|
||||||
|
private String mPhoneNumber;
|
||||||
|
private Timer mOnGoingCallTimer;
|
||||||
|
private int mCallingTime;
|
||||||
|
private boolean isClosing = false; // 新增:避免重复关闭页面
|
||||||
|
|
||||||
public static void actionStart(Context context, String phoneNumber,
|
// 对外静态接口(单例启动+外部关闭)
|
||||||
PhoneCallService.CallType callType) {
|
public static void actionStart(Context context, String phoneNumber, PhoneCallService.CallType callType) {
|
||||||
|
if (context == null || phoneNumber == null || callType == null) {
|
||||||
|
LogUtils.e(TAG, "actionStart: 入参为空,启动失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sIsActivityAlive) {
|
||||||
|
LogUtils.w(TAG, MI_ADAPT_TAG + " 已有活跃通话窗口,拒绝重复启动");
|
||||||
|
Toast.makeText(context, TOAST_CALLING, Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogUtils.d(TAG, MI_ADAPT_TAG + " 启动通话界面,号码=" + phoneNumber + ",类型=" + callType.name());
|
||||||
Intent intent = new Intent(context, PhoneCallActivity.class);
|
Intent intent = new Intent(context, PhoneCallActivity.class);
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
|
||||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, callType);
|
intent.putExtra("call_type", callType);
|
||||||
intent.putExtra(Intent.EXTRA_PHONE_NUMBER, phoneNumber);
|
intent.putExtra(Intent.EXTRA_PHONE_NUMBER, phoneNumber);
|
||||||
context.startActivity(intent);
|
context.startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void closePhoneCallActivity() {
|
||||||
|
LogUtils.d(TAG, "closePhoneCallActivity: 收到外部关闭指令");
|
||||||
|
if (sIsActivityAlive && sCloseHandler != null) {
|
||||||
|
sCloseHandler.sendEmptyMessage(MSG_CLOSE_ACTIVITY);
|
||||||
|
LogUtils.d(TAG, "closePhoneCallActivity: 关闭消息已发送");
|
||||||
|
} else {
|
||||||
|
LogUtils.w(TAG, "closePhoneCallActivity: 页面已销毁或Handler未初始化,关闭跳过");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生命周期方法区(按执行流程排序)
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_phone_call);
|
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面开始创建,SDK版本=" + Build.VERSION.SDK_INT);
|
||||||
|
|
||||||
|
// 单例双重校验,防止异常场景多实例
|
||||||
|
if (sIsActivityAlive) {
|
||||||
|
Toast.makeText(this, TOAST_CALLING, Toast.LENGTH_SHORT).show();
|
||||||
|
LogUtils.w(TAG, MI_ADAPT_TAG + " 拦截重复创建,即将关闭当前实例");
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sIsActivityAlive = false;
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_phone_call);
|
||||||
ActivityStack.getInstance().addActivity(this);
|
ActivityStack.getInstance().addActivity(this);
|
||||||
|
adaptLockScreenAndXiaomi();
|
||||||
|
initHandler();
|
||||||
initData();
|
initData();
|
||||||
initView();
|
initView();
|
||||||
|
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面创建完成");
|
||||||
}
|
|
||||||
|
|
||||||
private void initData() {
|
|
||||||
phoneCallManager = new PhoneCallManager(this);
|
|
||||||
onGoingCallTimer = new Timer();
|
|
||||||
if (getIntent() != null) {
|
|
||||||
phoneNumber = getIntent().getStringExtra(Intent.EXTRA_PHONE_NUMBER);
|
|
||||||
callType = (PhoneCallService.CallType) getIntent().getSerializableExtra(Intent.EXTRA_MIME_TYPES);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initView() {
|
|
||||||
int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
||||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION //hide navigationBar
|
|
||||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
|
||||||
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
|
|
||||||
getWindow().getDecorView().setSystemUiVisibility(uiOptions);
|
|
||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
|
|
||||||
|
|
||||||
tvCallNumberLabel = findViewById(R.id.tv_call_number_label);
|
|
||||||
tvCallNumber = findViewById(R.id.tv_call_number);
|
|
||||||
tvPickUp = findViewById(R.id.tv_phone_pick_up);
|
|
||||||
tvCallingTime = findViewById(R.id.tv_phone_calling_time);
|
|
||||||
tvHangUp = findViewById(R.id.tv_phone_hang_up);
|
|
||||||
|
|
||||||
tvCallNumber.setText(formatPhoneNumber(phoneNumber));
|
|
||||||
tvPickUp.setOnClickListener(this);
|
|
||||||
tvHangUp.setOnClickListener(this);
|
|
||||||
|
|
||||||
// 打进的电话
|
|
||||||
if (callType == PhoneCallService.CallType.CALL_IN) {
|
|
||||||
tvCallNumberLabel.setText("来电号码");
|
|
||||||
tvPickUp.setVisibility(View.VISIBLE);
|
|
||||||
} else if (callType == PhoneCallService.CallType.CALL_OUT) {
|
|
||||||
tvCallNumberLabel.setText("呼叫号码");
|
|
||||||
tvPickUp.setVisibility(View.GONE);
|
|
||||||
phoneCallManager.openSpeaker();
|
|
||||||
}
|
|
||||||
|
|
||||||
showOnLockScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void showOnLockScreen() {
|
|
||||||
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
|
|
||||||
WindowManager.LayoutParams.FLAG_FULLSCREEN |
|
|
||||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
|
|
||||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON,
|
|
||||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
|
|
||||||
WindowManager.LayoutParams.FLAG_FULLSCREEN |
|
|
||||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
|
|
||||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
if (v.getId() == R.id.tv_phone_pick_up) {
|
|
||||||
phoneCallManager.answer();
|
|
||||||
tvPickUp.setVisibility(View.GONE);
|
|
||||||
tvCallingTime.setVisibility(View.VISIBLE);
|
|
||||||
onGoingCallTimer.schedule(new TimerTask() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
callingTime++;
|
|
||||||
tvCallingTime.setText("通话中:" + getCallingTime());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 0, 1000);
|
|
||||||
} else if (v.getId() == R.id.tv_phone_hang_up) {
|
|
||||||
phoneCallManager.disconnect();
|
|
||||||
stopTimer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getCallingTime() {
|
|
||||||
int minute = callingTime / 60;
|
|
||||||
int second = callingTime % 60;
|
|
||||||
return (minute < 10 ? "0" + minute : minute) +
|
|
||||||
":" +
|
|
||||||
(second < 10 ? "0" + second : second);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stopTimer() {
|
|
||||||
if (onGoingCallTimer != null) {
|
|
||||||
onGoingCallTimer.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
callingTime = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
//MainActivity.updateCallLogFragment();
|
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面开始销毁");
|
||||||
phoneCallManager.destroy();
|
|
||||||
|
sIsActivityAlive = false;
|
||||||
|
isClosing = false;
|
||||||
|
stopTimer();
|
||||||
|
// 销毁通话管理器
|
||||||
|
if (mPhoneCallManager != null) {
|
||||||
|
mPhoneCallManager.destroy();
|
||||||
|
mPhoneCallManager = null;
|
||||||
|
LogUtils.d(TAG, "销毁通话管理器资源");
|
||||||
|
}
|
||||||
|
// 销毁Handler避免内存泄漏
|
||||||
|
if (sCloseHandler != null) {
|
||||||
|
sCloseHandler.removeCallbacksAndMessages(null);
|
||||||
|
sCloseHandler = null;
|
||||||
|
LogUtils.d(TAG, "销毁关闭Handler");
|
||||||
|
}
|
||||||
|
ActivityStack.getInstance().removeActivity(this);
|
||||||
|
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面销毁完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
if (isFinishing()) {
|
||||||
|
sIsActivityAlive = false;
|
||||||
|
LogUtils.d(TAG, MI_ADAPT_TAG + " 页面即将关闭,重置单例标记");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击事件回调
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
if (v == null) {
|
||||||
|
LogUtils.w(TAG, "onClick: 点击控件为空,忽略操作");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (v.getId()) {
|
||||||
|
case R.id.tv_phone_pick_up:
|
||||||
|
LogUtils.d(TAG, "onClick: 触发接听操作");
|
||||||
|
answerCall();
|
||||||
|
break;
|
||||||
|
case R.id.tv_phone_hang_up:
|
||||||
|
LogUtils.d(TAG, "onClick: 触发挂断操作,当前通话时长=" + mCallingTime + "秒");
|
||||||
|
hangUpCall();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LogUtils.w(TAG, "onClick: 未知点击事件,控件ID=" + v.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化方法区(按初始化顺序排列)
|
||||||
|
private void initHandler() {
|
||||||
|
sCloseHandler = new Handler(Looper.getMainLooper()) {
|
||||||
|
@Override
|
||||||
|
public void handleMessage(Message msg) {
|
||||||
|
super.handleMessage(msg);
|
||||||
|
if (msg.what == MSG_CLOSE_ACTIVITY) {
|
||||||
|
LogUtils.d(TAG, "handleMessage: 收到关闭消息,执行挂断逻辑");
|
||||||
|
hangUpCall();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
LogUtils.d(TAG, "initHandler: 关闭Handler初始化完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initData() {
|
||||||
|
LogUtils.d(TAG, "initData: 开始初始化业务数据");
|
||||||
|
mPhoneCallManager = PhoneCallManager.getInstance(this);
|
||||||
|
Intent intent = getIntent();
|
||||||
|
|
||||||
|
if (intent == null) {
|
||||||
|
LogUtils.e(TAG, "initData: 启动Intent为空,终止初始化");
|
||||||
|
removeFromRecentsAndFinish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mPhoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
|
||||||
|
mCallType = (PhoneCallService.CallType) intent.getSerializableExtra("call_type");
|
||||||
|
if (mPhoneNumber == null || mCallType == null) {
|
||||||
|
LogUtils.e(TAG, "initData: 通话号码或类型解析失败");
|
||||||
|
removeFromRecentsAndFinish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mOnGoingCallTimer = new Timer();
|
||||||
|
mCallingTime = 0;
|
||||||
|
LogUtils.d(TAG, "initData: 业务数据初始化完成,号码=" + mPhoneNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initView() {
|
||||||
|
LogUtils.d(TAG, "initView: 开始初始化界面控件");
|
||||||
|
// 修复沉浸式导航栏语法,适配小米全面屏
|
||||||
|
int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(uiOptions);
|
||||||
|
|
||||||
|
// 绑定控件
|
||||||
|
mTvCallNumberLabel = findViewById(R.id.tv_call_number_label);
|
||||||
|
mTvCallNumber = findViewById(R.id.tv_call_number);
|
||||||
|
mTvPickUp = findViewById(R.id.tv_phone_pick_up);
|
||||||
|
mTvCallingTime = findViewById(R.id.tv_phone_calling_time);
|
||||||
|
mTvHangUp = findViewById(R.id.tv_phone_hang_up);
|
||||||
|
|
||||||
|
// 设置控件属性
|
||||||
|
mTvCallNumber.setText(formatPhoneNumber(mPhoneNumber));
|
||||||
|
mTvPickUp.setOnClickListener(this);
|
||||||
|
mTvHangUp.setOnClickListener(this);
|
||||||
|
|
||||||
|
// 区分来电/去电UI样式
|
||||||
|
if (PhoneCallService.CallType.CALL_IN == mCallType) {
|
||||||
|
mTvCallNumberLabel.setText("来电号码");
|
||||||
|
mTvPickUp.setVisibility(View.VISIBLE);
|
||||||
|
mTvCallingTime.setVisibility(View.GONE);
|
||||||
|
} else if (PhoneCallService.CallType.CALL_OUT == mCallType) {
|
||||||
|
mTvCallNumberLabel.setText("呼叫号码");
|
||||||
|
mTvPickUp.setVisibility(View.GONE);
|
||||||
|
mTvCallingTime.setVisibility(View.VISIBLE);
|
||||||
|
mTvCallingTime.setText("通话中:00:00");
|
||||||
|
if (mPhoneCallManager != null) {
|
||||||
|
mPhoneCallManager.openSpeaker();
|
||||||
|
LogUtils.d(TAG, MI_ADAPT_TAG + " 去电模式自动开启免提");
|
||||||
|
}
|
||||||
|
startCallTimer();
|
||||||
|
}
|
||||||
|
LogUtils.d(TAG, "initView: 界面控件初始化完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 小米机型专属适配方法
|
||||||
|
private void adaptLockScreenAndXiaomi() {
|
||||||
|
LogUtils.d(TAG, MI_ADAPT_TAG + " 执行锁屏适配逻辑");
|
||||||
|
Window window = getWindow();
|
||||||
|
if (window == null) {
|
||||||
|
LogUtils.e(TAG, MI_ADAPT_TAG + " Window对象为空,适配失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flags = WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
||||||
|
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||||
|
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
|
||||||
|
|
||||||
|
// 小米机型额外添加解锁屏标志,解决MIUI锁屏拦截问题
|
||||||
|
if (Build.MANUFACTURER.equalsIgnoreCase("Xiaomi")) {
|
||||||
|
flags |= WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
|
||||||
|
LogUtils.d(TAG, MI_ADAPT_TAG + " 已添加小米机型专属锁屏适配标志");
|
||||||
|
}
|
||||||
|
window.addFlags(flags);
|
||||||
|
|
||||||
|
// 适配API29+锁屏新接口
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
setShowWhenLocked(true);
|
||||||
|
setTurnScreenOn(true);
|
||||||
|
LogUtils.d(TAG, MI_ADAPT_TAG + " 适配API29+锁屏接口完成");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通话核心业务方法
|
||||||
|
private void answerCall() {
|
||||||
|
LogUtils.d(TAG, "answerCall: 执行接听操作");
|
||||||
|
if (mPhoneCallManager == null) {
|
||||||
|
LogUtils.e(TAG, "answerCall: 通话管理器为空,接听失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mPhoneCallManager.answer();
|
||||||
|
mTvPickUp.setVisibility(View.GONE);
|
||||||
|
mTvCallingTime.setVisibility(View.VISIBLE);
|
||||||
|
mTvCallingTime.setText("通话中:00:00");
|
||||||
|
startCallTimer();
|
||||||
|
LogUtils.d(TAG, "answerCall: 接听操作完成,启动通话计时");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hangUpCall() {
|
||||||
|
if (isClosing) {
|
||||||
|
LogUtils.w(TAG, "hangUpCall: 挂断操作已执行,无需重复调用");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LogUtils.d(TAG, "hangUpCall: 执行挂断操作,当前时长=" + mCallingTime + "秒");
|
||||||
|
isClosing = true;
|
||||||
|
stopTimer();
|
||||||
|
if (mPhoneCallManager != null) {
|
||||||
|
mPhoneCallManager.disconnect();
|
||||||
|
LogUtils.d(TAG, "hangUpCall: 通话连接已断开");
|
||||||
|
}
|
||||||
|
// 延迟关闭页面,适配小米机型通话时序
|
||||||
|
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
removeFromRecentsAndFinish();
|
||||||
|
}
|
||||||
|
}, CLOSE_DELAY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 任务栈清理方法
|
||||||
|
private void removeFromRecentsAndFinish() {
|
||||||
|
if (isFinishing()) {
|
||||||
|
LogUtils.d(TAG, "removeFromRecentsAndFinish: 页面已在关闭中,无需重复操作");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
finishAndRemoveTask();
|
||||||
|
LogUtils.d(TAG, MI_ADAPT_TAG + " 移除任务栈并关闭页面");
|
||||||
|
} else {
|
||||||
|
finish();
|
||||||
|
LogUtils.d(TAG, "兼容低版本,关闭页面");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计时工具方法
|
||||||
|
private void startCallTimer() {
|
||||||
|
LogUtils.d(TAG, "startCallTimer: 启动通话计时器");
|
||||||
|
if (mOnGoingCallTimer == null) {
|
||||||
|
mOnGoingCallTimer = new Timer();
|
||||||
|
}
|
||||||
|
mOnGoingCallTimer.schedule(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
mCallingTime++;
|
||||||
|
mTvCallingTime.setText("通话中:" + formatCallingTime(mCallingTime));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 0, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopTimer() {
|
||||||
|
LogUtils.d(TAG, "stopTimer: 停止通话计时器");
|
||||||
|
if (mOnGoingCallTimer != null) {
|
||||||
|
mOnGoingCallTimer.cancel();
|
||||||
|
mOnGoingCallTimer = null;
|
||||||
|
}
|
||||||
|
mCallingTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助工具方法:格式化通话时长
|
||||||
|
private String formatCallingTime(int seconds) {
|
||||||
|
int minute = seconds / 60;
|
||||||
|
int second = seconds % 60;
|
||||||
|
String minuteStr = minute < 10 ? "0" + minute : String.valueOf(minute);
|
||||||
|
String secondStr = second < 10 ? "0" + second : String.valueOf(second);
|
||||||
|
return minuteStr + ":" + secondStr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,57 +6,199 @@ import android.os.Build;
|
|||||||
import android.telecom.Call;
|
import android.telecom.Call;
|
||||||
import android.telecom.VideoProfile;
|
import android.telecom.VideoProfile;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||||
|
* @Date 2025/12/15 20:11
|
||||||
|
* @Describe 通话核心管理类
|
||||||
|
* 功能:接听/挂断通话、免提控制、资源释放,适配API29-30及小米机型
|
||||||
|
*/
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.Q) // 匹配目标适配区间API29
|
||||||
public class PhoneCallManager {
|
public class PhoneCallManager {
|
||||||
|
// 常量定义区
|
||||||
|
public static final String TAG = "PhoneCallManager";
|
||||||
|
private static final String MI_ADAPT_TAG = "MiDeviceAdapt"; // 小米适配标识
|
||||||
|
private static final int VIDEO_PROFILE_AUDIO_ONLY = VideoProfile.STATE_AUDIO_ONLY;
|
||||||
|
private static final int AUDIO_MODE_BACKUP = -1; // 音频模式备份默认值
|
||||||
|
|
||||||
public static Call call;
|
// 成员属性区(按依赖优先级排序,移除静态call避免跨组件冲突)
|
||||||
|
private Context mContext;
|
||||||
|
private AudioManager mAudioManager;
|
||||||
|
private int mAudioModeBackup; // 备份原始音频模式,避免影响其他应用
|
||||||
|
private boolean mIsSpeakerOpened; // 免提状态标记,防止重复切换
|
||||||
|
|
||||||
private Context context;
|
// 构造方法(单例化改造,避免多实例冲突)
|
||||||
private AudioManager audioManager;
|
private static volatile PhoneCallManager sInstance;
|
||||||
|
public static PhoneCallManager getInstance(Context context) {
|
||||||
public PhoneCallManager(Context context) {
|
if (context == null) {
|
||||||
this.context = context;
|
LogUtils.e(TAG, "getInstance: 上下文为空,初始化失败");
|
||||||
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
return null;
|
||||||
|
}
|
||||||
|
if (sInstance == null) {
|
||||||
|
synchronized (PhoneCallManager.class) {
|
||||||
|
if (sInstance == null) {
|
||||||
|
sInstance = new PhoneCallManager(context.getApplicationContext()); // 用应用上下文,避免内存泄漏
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 私有构造,禁止外部实例化
|
||||||
|
private PhoneCallManager(Context context) {
|
||||||
|
LogUtils.d(TAG, MI_ADAPT_TAG + " 初始化通话管理类");
|
||||||
|
this.mContext = context;
|
||||||
|
this.mAudioModeBackup = AUDIO_MODE_BACKUP;
|
||||||
|
this.mIsSpeakerOpened = false;
|
||||||
|
initAudioManager();
|
||||||
|
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话管理类初始化完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化辅助方法
|
||||||
|
private void initAudioManager() {
|
||||||
|
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||||
|
if (mAudioManager != null) {
|
||||||
|
// 备份原始音频模式(小米机型切换后需恢复,避免外放异常)
|
||||||
|
mAudioModeBackup = mAudioManager.getMode();
|
||||||
|
LogUtils.d(TAG, "音频管理器初始化成功,原始模式备份:" + mAudioModeBackup);
|
||||||
|
} else {
|
||||||
|
LogUtils.e(TAG, "音频管理器初始化失败,将影响通话音频控制");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 核心业务方法(按使用场景排序,强化小米适配+容错)
|
||||||
/**
|
/**
|
||||||
* 接听电话
|
* 接听电话,默认音频通话模式
|
||||||
*/
|
*/
|
||||||
public void answer() {
|
public void answer() {
|
||||||
if (call != null) {
|
LogUtils.d(TAG, "执行接听通话操作");
|
||||||
call.answer(VideoProfile.STATE_AUDIO_ONLY);
|
// 从PhoneCallService的静态管理器获取通话对象,统一数据源
|
||||||
openSpeaker();
|
Call currentCall = PhoneCallService.PhoneCallManager.call;
|
||||||
|
if (currentCall == null) {
|
||||||
|
LogUtils.e(TAG, "接听失败:通话对象为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验通话状态,避免重复接听(小米机型状态变更延迟)
|
||||||
|
if (currentCall.getState() != Call.STATE_RINGING) {
|
||||||
|
LogUtils.w(TAG, MI_ADAPT_TAG + " 非响铃状态,无需接听,当前状态:" + currentCall.getState());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
currentCall.answer(VIDEO_PROFILE_AUDIO_ONLY);
|
||||||
|
openSpeaker(); // 接听后自动开免提
|
||||||
|
LogUtils.d(TAG, "通话接听成功,自动开启免提");
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
LogUtils.e(TAG, MI_ADAPT_TAG + " 接听权限不足(需android.permission.ANSWER_PHONE_CALLS)", e);
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
LogUtils.e(TAG, MI_ADAPT_TAG + " 通话状态异常,无法接听", e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "接听通话异常", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 断开电话,包括来电时的拒接以及接听后的挂断
|
* 断开通话(支持来电拒接、通话中挂断)
|
||||||
*/
|
*/
|
||||||
public void disconnect() {
|
public void disconnect() {
|
||||||
if (call != null) {
|
LogUtils.d(TAG, "执行断开通话操作");
|
||||||
call.disconnect();
|
Call currentCall = PhoneCallService.PhoneCallManager.call;
|
||||||
|
if (currentCall == null) {
|
||||||
|
LogUtils.e(TAG, "挂断失败:通话对象为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验通话状态,避免重复挂断
|
||||||
|
if (currentCall.getState() == Call.STATE_DISCONNECTED) {
|
||||||
|
LogUtils.w(TAG, MI_ADAPT_TAG + " 通话已断开,无需重复操作");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
currentCall.disconnect();
|
||||||
|
closeSpeaker(); // 挂断后关闭免提+恢复音频模式
|
||||||
|
LogUtils.d(TAG, "通话断开成功");
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
LogUtils.e(TAG, MI_ADAPT_TAG + " 挂断权限不足(需android.permission.CALL_PHONE)", e);
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
LogUtils.e(TAG, MI_ADAPT_TAG + " 通话状态异常,无法挂断", e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "断开通话异常", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开免提
|
* 打开免提,适配小米机型音频通道切换(解决MIUI音频混乱)
|
||||||
*/
|
*/
|
||||||
public void openSpeaker() {
|
public void openSpeaker() {
|
||||||
if (audioManager != null) {
|
LogUtils.d(TAG, "执行打开免提操作");
|
||||||
audioManager.setMode(AudioManager.MODE_IN_CALL);
|
if (mAudioManager == null) {
|
||||||
audioManager.setSpeakerphoneOn(true);
|
LogUtils.e(TAG, "打开免提失败:音频管理器未初始化");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mIsSpeakerOpened) {
|
||||||
|
LogUtils.w(TAG, "免提已开启,无需重复操作");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 小米机型适配步骤:1. 设置通话模式 2. 关闭静音 3. 开启免提(固定顺序)
|
||||||
|
mAudioManager.setMode(AudioManager.MODE_IN_CALL);
|
||||||
|
mAudioManager.setStreamMute(AudioManager.STREAM_VOICE_CALL, false); // 确保通话音频不静音
|
||||||
|
mAudioManager.setSpeakerphoneOn(true);
|
||||||
|
|
||||||
|
mIsSpeakerOpened = true;
|
||||||
|
LogUtils.d(TAG, MI_ADAPT_TAG + " 免提开启成功,当前模式:" + mAudioManager.getMode());
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
LogUtils.e(TAG, MI_ADAPT_TAG + " 音频控制权限不足", e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "打开免提异常", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 销毁资源
|
* 新增:关闭免提(挂断/切换场景调用,修复小米音频残留)
|
||||||
|
*/
|
||||||
|
public void closeSpeaker() {
|
||||||
|
LogUtils.d(TAG, "执行关闭免提操作");
|
||||||
|
if (mAudioManager == null || !mIsSpeakerOpened) {
|
||||||
|
LogUtils.w(TAG, "免提未开启或音频管理器为空,无需操作");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
mAudioManager.setSpeakerphoneOn(false);
|
||||||
|
// 恢复原始音频模式(关键:小米机型不恢复会导致其他应用外放异常)
|
||||||
|
if (mAudioModeBackup != AUDIO_MODE_BACKUP) {
|
||||||
|
mAudioManager.setMode(mAudioModeBackup);
|
||||||
|
LogUtils.d(TAG, MI_ADAPT_TAG + " 恢复原始音频模式:" + mAudioModeBackup);
|
||||||
|
}
|
||||||
|
mIsSpeakerOpened = false;
|
||||||
|
LogUtils.d(TAG, "免提关闭成功");
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, MI_ADAPT_TAG + " 关闭免提异常", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁资源,避免内存泄漏+音频残留(适配小米内存管理)
|
||||||
*/
|
*/
|
||||||
public void destroy() {
|
public void destroy() {
|
||||||
call = null;
|
LogUtils.d(TAG, "开始销毁通话管理资源");
|
||||||
context = null;
|
closeSpeaker(); // 销毁前强制关闭免提+恢复音频模式
|
||||||
audioManager = null;
|
// 释放资源(应用上下文无需主动置空,避免空指针)
|
||||||
|
mAudioManager = null;
|
||||||
|
sInstance = null; // 单例置空,下次重新初始化
|
||||||
|
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话管理资源销毁完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增:获取当前免提状态(供UI层同步显示)
|
||||||
|
*/
|
||||||
|
public boolean isSpeakerOpened() {
|
||||||
|
return mIsSpeakerOpened;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,240 +16,247 @@ import cc.winboll.studio.libappbase.LogUtils;
|
|||||||
* @author aJIEw, ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
* @author aJIEw, ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||||
* @see PhoneCallActivity
|
* @see PhoneCallActivity
|
||||||
* @see android.telecom.InCallService
|
* @see android.telecom.InCallService
|
||||||
* 适配:Java7 语法 + Android API29-30 | 移除录音功能 | 强化稳定性与容错性
|
* 适配:Java7 语法 + Android API29 - 30 | 移除录音功能 | 强化小米设备稳定性与容错性
|
||||||
*/
|
*/
|
||||||
@RequiresApi(api = 29) // 适配API29+,替代Build.VERSION_CODES.M,匹配InCallService实际最低要求
|
@RequiresApi(api = 29)
|
||||||
public class PhoneCallService extends InCallService {
|
public class PhoneCallService extends InCallService {
|
||||||
|
// 常量定义区
|
||||||
// ====================== 常量定义区(精简必要常量,无冗余) ======================
|
|
||||||
public static final String TAG = "PhoneCallService";
|
public static final String TAG = "PhoneCallService";
|
||||||
|
// 小米设备适配标识,便于日志区分
|
||||||
|
private static final String MI_DEVICE_TAG = "MiDeviceAdapt";
|
||||||
|
|
||||||
// ====================== 成员属性区(按功能归类,命名规范) ======================
|
// 成员属性区(按依赖顺序排列)
|
||||||
private Call.Callback mCallCallback; // 通话状态回调(统一管理,便于销毁)
|
private Call.Callback mCallCallback;
|
||||||
|
private AudioManager mAudioManager;
|
||||||
|
|
||||||
// ====================== 内部枚举类(提前定义,便于业务调用) ======================
|
// 内部枚举类(通话类型定义)
|
||||||
public enum CallType {
|
public enum CallType {
|
||||||
CALL_IN, // 来电
|
CALL_IN, // 来电
|
||||||
CALL_OUT // 去电
|
CALL_OUT // 去电
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================== Service生命周期方法区(按执行顺序排列) ======================
|
// Service生命周期方法区(按执行流程排序)
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
LogUtils.d(TAG, "===== onCreate: 通话监听服务启动 =====");
|
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话监听服务启动");
|
||||||
// 初始化通话状态回调(提前初始化,避免重复创建)
|
initAudioManager();
|
||||||
initCallCallback();
|
initCallCallback();
|
||||||
LogUtils.d(TAG, "===== onCreate: 服务初始化完成 =====");
|
LogUtils.d(TAG, MI_DEVICE_TAG + " 服务初始化完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCallAdded(Call call) {
|
public void onCallAdded(Call call) {
|
||||||
super.onCallAdded(call);
|
super.onCallAdded(call);
|
||||||
LogUtils.d(TAG, "onCallAdded: 检测到新通话,开始处理");
|
LogUtils.d(TAG, "检测到新通话");
|
||||||
|
|
||||||
// 空指针防护:避免通话对象为空导致崩溃
|
|
||||||
if (call == null) {
|
if (call == null) {
|
||||||
LogUtils.e(TAG, "onCallAdded: 通话对象为空,跳过处理");
|
LogUtils.e(TAG, "通话对象为空,跳过处理");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册通话状态回调
|
// 双重校验回调,避免重复注册
|
||||||
call.registerCallback(mCallCallback);
|
if (mCallCallback != null) {
|
||||||
|
call.registerCallback(mCallCallback);
|
||||||
|
}
|
||||||
|
// 绑定通话对象到管理器,供UI层调用
|
||||||
PhoneCallManager.call = call;
|
PhoneCallManager.call = call;
|
||||||
LogUtils.d(TAG, "onCallAdded: 已注册通话回调,通话对象绑定完成");
|
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话回调注册成功,对象绑定完成");
|
||||||
|
|
||||||
// 判断通话类型(来电/去电)
|
|
||||||
CallType callType = judgeCallType(call);
|
CallType callType = judgeCallType(call);
|
||||||
if (callType != null) {
|
if (callType != null) {
|
||||||
// 处理有效通话(音量控制、规则校验、启动通话界面)
|
|
||||||
handleValidCall(call, callType);
|
handleValidCall(call, callType);
|
||||||
} else {
|
} else {
|
||||||
LogUtils.w(TAG, "onCallAdded: 无法识别通话类型,通话状态=" + call.getState());
|
LogUtils.w(TAG, "无法识别通话类型,状态码:" + call.getState());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCallRemoved(Call call) {
|
public void onCallRemoved(Call call) {
|
||||||
super.onCallRemoved(call);
|
super.onCallRemoved(call);
|
||||||
LogUtils.d(TAG, "onCallRemoved: 通话结束,开始清理资源");
|
LogUtils.d(TAG, "通话结束,开始清理资源");
|
||||||
|
if (call != null && mCallCallback != null) {
|
||||||
// 空指针防护:避免通话对象为空导致崩溃
|
|
||||||
if (call != null) {
|
|
||||||
call.unregisterCallback(mCallCallback);
|
call.unregisterCallback(mCallCallback);
|
||||||
LogUtils.d(TAG, "onCallRemoved: 已注销通话回调");
|
LogUtils.d(TAG, "通话回调已注销");
|
||||||
}
|
}
|
||||||
|
|
||||||
PhoneCallManager.call = null;
|
// 延迟置空通话对象,避免UI层挂断时对象已被释放(适配小米机型时序)
|
||||||
LogUtils.d(TAG, "onCallRemoved: 通话资源清理完成");
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
// 延迟200ms,确保PhoneCallActivity挂断逻辑执行完成
|
||||||
|
Thread.sleep(200);
|
||||||
|
PhoneCallManager.call = null;
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
LogUtils.e(TAG, MI_DEVICE_TAG + " 延迟置空通话对象异常", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
|
||||||
|
PhoneCallActivity.closePhoneCallActivity();
|
||||||
|
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话资源清理完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
LogUtils.d(TAG, "onDestroy: 通话监听服务开始销毁");
|
LogUtils.d(TAG, "服务开始销毁");
|
||||||
// 更新通话记录列表
|
|
||||||
CallLogFragment.updateCallLogFragment();
|
CallLogFragment.updateCallLogFragment();
|
||||||
LogUtils.d(TAG, "onDestroy: 通话监听服务销毁完成");
|
// 释放资源,适配小米设备内存管理,避免内存泄漏
|
||||||
|
mCallCallback = null;
|
||||||
|
mAudioManager = null;
|
||||||
|
LogUtils.d(TAG, MI_DEVICE_TAG + " 服务销毁完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化方法区
|
||||||
|
private void initAudioManager() {
|
||||||
|
mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
|
||||||
|
if (mAudioManager == null) {
|
||||||
|
LogUtils.e(TAG, MI_DEVICE_TAG + " 获取音频管理器失败");
|
||||||
|
} else {
|
||||||
|
LogUtils.d(TAG, MI_DEVICE_TAG + " 音频管理器初始化成功");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================== 核心初始化方法区 ======================
|
|
||||||
/**
|
|
||||||
* 初始化通话状态回调,统一管理通话状态变更逻辑
|
|
||||||
*/
|
|
||||||
private void initCallCallback() {
|
private void initCallCallback() {
|
||||||
mCallCallback = new Call.Callback() {
|
mCallCallback = new Call.Callback() {
|
||||||
@Override
|
@Override
|
||||||
public void onStateChanged(Call call, int state) {
|
public void onStateChanged(Call call, int state) {
|
||||||
super.onStateChanged(call, state);
|
super.onStateChanged(call, state);
|
||||||
LogUtils.d(TAG, "onStateChanged: 通话状态变更,状态码=" + state + ",状态描述=" + getCallStateDesc(state));
|
if (call == null) {
|
||||||
|
LogUtils.e(TAG, "onStateChanged: 通话对象为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String stateDesc = getCallStateDesc(state);
|
||||||
|
LogUtils.d(TAG, "通话状态变更:" + stateDesc + "(状态码:" + state + ")");
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case TelephonyManager.CALL_STATE_IDLE:
|
|
||||||
// 通话空闲(挂断后),无需额外处理(原录音停止逻辑已删除)
|
|
||||||
LogUtils.d(TAG, "onStateChanged: 通话进入空闲状态");
|
|
||||||
break;
|
|
||||||
case Call.STATE_DISCONNECTED:
|
case Call.STATE_DISCONNECTED:
|
||||||
// 通话断开,关闭通话界面
|
// 双重校验,避免重复关闭页面
|
||||||
ActivityStack.getInstance().finishActivity(PhoneCallActivity.class);
|
if (ActivityStack.getInstance().getActivity(PhoneCallActivity.class) != null) {
|
||||||
LogUtils.d(TAG, "onStateChanged: 通话断开,已关闭通话界面");
|
ActivityStack.getInstance().finishActivity(PhoneCallActivity.class);
|
||||||
|
LogUtils.d(TAG, "通话界面已关闭");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
// 保留其他状态分支,便于后续扩展,无冗余逻辑
|
|
||||||
case Call.STATE_ACTIVE:
|
case Call.STATE_ACTIVE:
|
||||||
LogUtils.d(TAG, "onStateChanged: 通话进入活跃状态");
|
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话进入活跃状态,适配音频通道");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
LogUtils.d(TAG, "initCallCallback: 通话状态回调初始化完成");
|
LogUtils.d(TAG, "通话状态回调初始化完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================== 核心业务处理方法区 ======================
|
// 核心业务处理方法区
|
||||||
/**
|
|
||||||
* 判断通话类型(来电/去电)
|
|
||||||
* @param call 通话对象
|
|
||||||
* @return 通话类型枚举,无法识别返回null
|
|
||||||
*/
|
|
||||||
private CallType judgeCallType(Call call) {
|
private CallType judgeCallType(Call call) {
|
||||||
|
if (call == null) {
|
||||||
|
LogUtils.e(TAG, "judgeCallType: 通话对象为空");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
int callState = call.getState();
|
int callState = call.getState();
|
||||||
if (callState == Call.STATE_RINGING) {
|
if (callState == Call.STATE_RINGING) {
|
||||||
LogUtils.d(TAG, "judgeCallType: 通话状态为响铃,识别为来电");
|
LogUtils.d(TAG, "识别为来电");
|
||||||
return CallType.CALL_IN;
|
return CallType.CALL_IN;
|
||||||
} else if (callState == Call.STATE_CONNECTING) {
|
} else if (callState == Call.STATE_CONNECTING) {
|
||||||
LogUtils.d(TAG, "judgeCallType: 通话状态为连接中,识别为去电");
|
LogUtils.d(TAG, "识别为去电");
|
||||||
return CallType.CALL_OUT;
|
return CallType.CALL_OUT;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private boolean handleValidCall(Call call, CallType callType) {
|
||||||
* 处理有效通话(音量控制、拦截规则校验、启动通话界面)
|
if (call == null || callType == null) {
|
||||||
* @param call 通话对象
|
LogUtils.e(TAG, "handleValidCall: 通话对象或类型为空");
|
||||||
* @param callType 通话类型(来电/去电)
|
return false;
|
||||||
*/
|
}
|
||||||
private void handleValidCall(Call call, CallType callType) {
|
|
||||||
// 1. 获取通话详情与号码(多层空指针防护)
|
|
||||||
Call.Details callDetails = call.getDetails();
|
Call.Details callDetails = call.getDetails();
|
||||||
if (callDetails == null || callDetails.getHandle() == null) {
|
if (callDetails == null || callDetails.getHandle() == null) {
|
||||||
LogUtils.e(TAG, "handleValidCall: 通话详情或号码信息为空,跳过后续处理");
|
LogUtils.e(TAG, "通话详情缺失,处理终止");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String phoneNumber = callDetails.getHandle().getSchemeSpecificPart();
|
String phoneNumber = callDetails.getHandle().getSchemeSpecificPart();
|
||||||
LogUtils.d(TAG, "handleValidCall: 开始处理通话,号码=" + phoneNumber + ",类型=" + callType);
|
LogUtils.d(TAG, "处理通话:号码=" + phoneNumber + ",类型=" + callType.name());
|
||||||
|
|
||||||
// 2. 初始化音频管理器(音量控制核心)
|
if (mAudioManager == null) {
|
||||||
AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
|
LogUtils.e(TAG, "音频管理器未初始化");
|
||||||
if (audioManager == null) {
|
|
||||||
LogUtils.e(TAG, "handleValidCall: 获取音频管理器失败,无法处理音量控制");
|
|
||||||
// 音量控制失败仍启动通话界面,保障基础功能可用
|
|
||||||
PhoneCallActivity.actionStart(this, phoneNumber, callType);
|
PhoneCallActivity.actionStart(this, phoneNumber, callType);
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 处理铃声音量(恢复配置音量、拦截时静音)
|
if (checkRulesAndHandleRingerVolumeControl(phoneNumber, call)) {
|
||||||
handleRingerVolumeControl(audioManager, phoneNumber, call);
|
PhoneCallActivity.actionStart(this, phoneNumber, callType);
|
||||||
|
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话界面启动成功");
|
||||||
// 4. 校验通过,启动通话界面(拦截场景已提前返回,此处直接启动)
|
return true;
|
||||||
PhoneCallActivity.actionStart(this, phoneNumber, callType);
|
}
|
||||||
LogUtils.d(TAG, "handleValidCall: 通话校验通过,已启动通话界面");
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private boolean checkRulesAndHandleRingerVolumeControl(String phoneNumber, Call call) {
|
||||||
* 铃声音量控制(恢复应用配置音量、拦截号码静音处理)
|
if (mAudioManager == null || phoneNumber == null || call == null) {
|
||||||
* @param audioManager 音频管理器
|
LogUtils.e(TAG, "checkRulesAndHandleRingerVolumeControl: 入参为空");
|
||||||
* @param phoneNumber 通话号码
|
return false;
|
||||||
* @param call 通话对象(用于拦截时断开通话)
|
}
|
||||||
*/
|
|
||||||
private void handleRingerVolumeControl(AudioManager audioManager, String phoneNumber, Call call) {
|
int currentVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_RING);
|
||||||
// 3.1 获取当前铃声音量
|
LogUtils.d(TAG, "当前铃声音量:" + currentVolume);
|
||||||
int currentRingerVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
|
|
||||||
LogUtils.d(TAG, "handleRingerVolumeControl: 当前铃声音量=" + currentRingerVolume);
|
|
||||||
|
|
||||||
// 3.2 加载/初始化铃声音量配置
|
|
||||||
RingTongBean ringTongBean = RingTongBean.loadBean(this, RingTongBean.class);
|
RingTongBean ringTongBean = RingTongBean.loadBean(this, RingTongBean.class);
|
||||||
if (ringTongBean == null) {
|
if (ringTongBean == null) {
|
||||||
ringTongBean = new RingTongBean();
|
ringTongBean = new RingTongBean();
|
||||||
RingTongBean.saveBean(this, ringTongBean);
|
RingTongBean.saveBean(this, ringTongBean);
|
||||||
LogUtils.d(TAG, "handleRingerVolumeControl: 铃声音量配置未初始化,已自动创建默认配置");
|
LogUtils.d(TAG, "初始化默认铃音配置");
|
||||||
}
|
}
|
||||||
int configRingerVolume = ringTongBean.getStreamVolume();
|
final int configVolume = ringTongBean.getStreamVolume();
|
||||||
LogUtils.d(TAG, "handleRingerVolumeControl: 应用配置铃声音量=" + configRingerVolume);
|
|
||||||
|
|
||||||
// 3.3 恢复应用配置音量(当前音量与配置不一致时调整)
|
|
||||||
try {
|
try {
|
||||||
if (currentRingerVolume != configRingerVolume) {
|
// 小米机型适配:调整音量时添加权限校验
|
||||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, configRingerVolume, 0);
|
if (currentVolume != configVolume) {
|
||||||
LogUtils.d(TAG, "handleRingerVolumeControl: 已将铃声音量恢复为应用配置值");
|
mAudioManager.setStreamVolume(AudioManager.STREAM_RING, configVolume, 0);
|
||||||
} else {
|
LogUtils.d(TAG, MI_DEVICE_TAG + " 铃声音量调整为配置值:" + configVolume);
|
||||||
LogUtils.d(TAG, "handleRingerVolumeControl: 当前音量与配置一致,无需调整");
|
|
||||||
}
|
}
|
||||||
} catch (SecurityException e) {
|
} catch (SecurityException e) {
|
||||||
LogUtils.e(TAG, "handleRingerVolumeControl: 恢复铃声音量失败,权限不足", e);
|
LogUtils.e(TAG, "音量调整失败,权限不足", e);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3.4 校验拦截规则,拦截号码静音+断开通话
|
// 校验拦截规则
|
||||||
if (!Rules.getInstance(this).isAllowed(phoneNumber)) {
|
if (!Rules.getInstance(this).isAllowed(phoneNumber)) {
|
||||||
LogUtils.d(TAG, "handleRingerVolumeControl: 号码=" + phoneNumber + " 命中拦截规则,开始拦截处理");
|
LogUtils.d(TAG, "号码" + phoneNumber + "命中拦截规则");
|
||||||
try {
|
try {
|
||||||
// 静音处理
|
// 拦截时静音并挂断
|
||||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, 0, 0);
|
mAudioManager.setStreamVolume(AudioManager.STREAM_RING, 0, 0);
|
||||||
LogUtils.d(TAG, "handleRingerVolumeControl: 已将铃声音量设为0(静音)");
|
call.disconnect();
|
||||||
|
LogUtils.d(TAG, MI_DEVICE_TAG + " 拦截通话已挂断并静音");
|
||||||
|
|
||||||
|
// 延迟恢复音量,适配小米机型音频通道延迟
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
Thread.sleep(500);
|
||||||
|
if (mAudioManager != null) {
|
||||||
|
mAudioManager.setStreamVolume(AudioManager.STREAM_RING, configVolume, 0);
|
||||||
|
LogUtils.d(TAG, MI_DEVICE_TAG + " 延迟恢复铃音配置");
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
LogUtils.e(TAG, "恢复音量线程中断", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
} catch (SecurityException e) {
|
} catch (SecurityException e) {
|
||||||
LogUtils.e(TAG, "handleRingerVolumeControl: 拦截静音失败,权限不足", e);
|
LogUtils.e(TAG, "拦截静音失败", e);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
// 断开通话
|
|
||||||
call.disconnect();
|
|
||||||
LogUtils.d(TAG, "handleRingerVolumeControl: 已断开拦截通话");
|
|
||||||
|
|
||||||
// 延迟恢复音量(防止第一声铃声响动)
|
|
||||||
try {
|
|
||||||
Thread.sleep(500);
|
|
||||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, configRingerVolume, 0);
|
|
||||||
LogUtils.d(TAG, "handleRingerVolumeControl: 延迟500ms后,已恢复铃声音量为配置值");
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
LogUtils.e(TAG, "handleRingerVolumeControl: 延迟恢复音量失败,线程被中断", e);
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
LogUtils.e(TAG, "handleRingerVolumeControl: 恢复音量失败,权限不足", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 拦截完成,直接返回,不启动通话界面
|
|
||||||
LogUtils.d(TAG, "handleRingerVolumeControl: 拦截处理完成,跳过通话界面启动");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
LogUtils.d(TAG, "handleRingerVolumeControl: 号码=" + phoneNumber + " 未命中拦截规则,音量控制完成");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================== 辅助工具方法区 ======================
|
// 辅助工具方法区:解析通话状态描述
|
||||||
/**
|
|
||||||
* 通话状态码转文字描述(便于日志查看,快速定位状态)
|
|
||||||
* @param state 通话状态码(TelephonyManager/Call 中的常量)
|
|
||||||
* @return 状态文字描述
|
|
||||||
*/
|
|
||||||
private String getCallStateDesc(int state) {
|
private String getCallStateDesc(int state) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case TelephonyManager.CALL_STATE_RINGING:
|
case TelephonyManager.CALL_STATE_RINGING:
|
||||||
@@ -257,16 +264,21 @@ public class PhoneCallService extends InCallService {
|
|||||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||||
return "通话中";
|
return "通话中";
|
||||||
case TelephonyManager.CALL_STATE_IDLE:
|
case TelephonyManager.CALL_STATE_IDLE:
|
||||||
return "空闲(未通话/已挂断)";
|
return "空闲";
|
||||||
case Call.STATE_ACTIVE:
|
case Call.STATE_ACTIVE:
|
||||||
return "通话活跃";
|
return "通话活跃";
|
||||||
case Call.STATE_CONNECTING:
|
case Call.STATE_CONNECTING:
|
||||||
return "通话连接中";
|
return "连接中";
|
||||||
case Call.STATE_DISCONNECTED:
|
case Call.STATE_DISCONNECTED:
|
||||||
return "通话已断开";
|
return "已断开";
|
||||||
default:
|
default:
|
||||||
return "未知状态";
|
return "未知状态";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 静态内部类:统一管理通话对象,避免跨组件对象混乱
|
||||||
|
public static class PhoneCallManager {
|
||||||
|
public static Call call;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -337,11 +337,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="其他:"/>
|
android:text="其他:"/>
|
||||||
|
|
||||||
<cc.winboll.studio.libaes.views.ADsControlView
|
|
||||||
android:id="@+id/ads_control_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -356,6 +351,11 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<cc.winboll.studio.libaes.views.ADsControlView
|
||||||
|
android:id="@+id/ads_control_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ def genVersionName(def versionName){
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
||||||
// 1. compileSdkVersion:必须 ≥ targetSdkVersion,建议直接等于 targetSdkVersion(30)
|
// 关键:改为你已安装的 SDK 32(≥ targetSdkVersion 30,兼容已安装环境)
|
||||||
compileSdkVersion 30
|
compileSdkVersion 32
|
||||||
|
|
||||||
// 2. buildToolsVersion:需匹配 compileSdkVersion,建议使用 30.x.x 最新稳定版(无需高于 compileSdkVersion)
|
// 直接使用已安装的构建工具 33.0.3(无需修改)
|
||||||
buildToolsVersion "30.0.3" // 这是 30 对应的最新稳定版,避免使用 beta 版
|
buildToolsVersion "33.0.3"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "cc.winboll.studio.winboll"
|
applicationId "cc.winboll.studio.winboll"
|
||||||
@@ -43,6 +43,12 @@ android {
|
|||||||
packagingOptions {
|
packagingOptions {
|
||||||
doNotStrip "*/*/libmimo_1011.so"
|
doNotStrip "*/*/libmimo_1011.so"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
jniLibs.srcDirs = ['libs'] // 若SO库放在libs目录下
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -63,6 +69,10 @@ dependencies {
|
|||||||
api 'io.github.medyo:android-about-page:2.0.0'
|
api 'io.github.medyo:android-about-page:2.0.0'
|
||||||
// 网络连接类库
|
// 网络连接类库
|
||||||
api 'com.squareup.okhttp3:okhttp:4.4.1'
|
api 'com.squareup.okhttp3:okhttp:4.4.1'
|
||||||
|
// OkHttp网络请求
|
||||||
|
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
|
||||||
|
// FastJSON解析
|
||||||
|
implementation 'com.alibaba:fastjson:1.2.76'
|
||||||
|
|
||||||
// AndroidX 类库
|
// AndroidX 类库
|
||||||
api 'androidx.appcompat:appcompat:1.1.0'
|
api 'androidx.appcompat:appcompat:1.1.0'
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
#Sun Dec 07 04:17:43 GMT 2025
|
#Tue Jan 06 06:07:46 GMT 2026
|
||||||
stageCount=8
|
stageCount=9
|
||||||
libraryProject=
|
libraryProject=
|
||||||
baseVersion=15.11
|
baseVersion=15.11
|
||||||
publishVersion=15.11.7
|
publishVersion=15.11.8
|
||||||
buildCount=1
|
buildCount=10
|
||||||
baseBetaVersion=15.11.8
|
baseBetaVersion=15.11.9
|
||||||
|
|||||||
BIN
winboll/libs/libWeWorkSpecSDK.so
Normal file
BIN
winboll/libs/libWeWorkSpecSDK.so
Normal file
Binary file not shown.
@@ -278,6 +278,10 @@
|
|||||||
|
|
||||||
<activity android:name="cc.winboll.studio.winboll.activities.SettingsActivity"/>
|
<activity android:name="cc.winboll.studio.winboll.activities.SettingsActivity"/>
|
||||||
|
|
||||||
|
<activity android:name="cc.winboll.studio.winboll.unittest.TestWeWorkSpecSDK"/>
|
||||||
|
|
||||||
|
<activity android:name="cc.winboll.studio.winboll.activities.WXPayActivity"/>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package cc.winboll.studio.winboll;
|
package cc.winboll.studio.winboll;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
@@ -8,20 +8,17 @@ import android.view.Menu;
|
|||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
|
import cc.winboll.studio.libaes.activitys.AboutActivity;
|
||||||
import cc.winboll.studio.libaes.activitys.DrawerFragmentActivity;
|
import cc.winboll.studio.libaes.activitys.DrawerFragmentActivity;
|
||||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
|
||||||
import cc.winboll.studio.libaes.models.APPInfo;
|
import cc.winboll.studio.libaes.models.APPInfo;
|
||||||
import cc.winboll.studio.libaes.models.DrawerMenuBean;
|
import cc.winboll.studio.libaes.models.DrawerMenuBean;
|
||||||
import cc.winboll.studio.libaes.unittests.TestAButtonFragment;
|
|
||||||
import cc.winboll.studio.libaes.unittests.TestViewPageFragment;
|
|
||||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
import cc.winboll.studio.winboll.R;
|
import cc.winboll.studio.winboll.R;
|
||||||
import cc.winboll.studio.winboll.activities.SettingsActivity;
|
import cc.winboll.studio.winboll.activities.SettingsActivity;
|
||||||
|
import cc.winboll.studio.winboll.activities.WXPayActivity;
|
||||||
import cc.winboll.studio.winboll.fragments.BrowserFragment;
|
import cc.winboll.studio.winboll.fragments.BrowserFragment;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import android.content.Intent;
|
|
||||||
import cc.winboll.studio.libaes.activitys.AboutActivity;
|
|
||||||
|
|
||||||
public class MainActivity extends DrawerFragmentActivity {
|
public class MainActivity extends DrawerFragmentActivity {
|
||||||
|
|
||||||
@@ -152,6 +149,8 @@ public class MainActivity extends DrawerFragmentActivity {
|
|||||||
}
|
}
|
||||||
} else if (nItemId == R.id.item_settings) {
|
} else if (nItemId == R.id.item_settings) {
|
||||||
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), SettingsActivity.class);
|
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), SettingsActivity.class);
|
||||||
|
} else if (nItemId == R.id.item_wxpayactivity) {
|
||||||
|
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), WXPayActivity.class);
|
||||||
} else if (nItemId == cc.winboll.studio.libaes.R.id.item_about) {
|
} else if (nItemId == cc.winboll.studio.libaes.R.id.item_about) {
|
||||||
Intent intent = new Intent(getApplicationContext(), AboutActivity.class);
|
Intent intent = new Intent(getApplicationContext(), AboutActivity.class);
|
||||||
APPInfo appInfo = genDefaultAPPInfo();
|
APPInfo appInfo = genDefaultAPPInfo();
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package cc.winboll.studio.winboll;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信支付配置类
|
||||||
|
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2026/01/07
|
||||||
|
*/
|
||||||
|
public class WxPayConfig {
|
||||||
|
// ========== 核心修改点:替换为你的服务端地址 ==========
|
||||||
|
// 服务端IP/域名 + 端口(Docker部署的服务端,需确保安卓端可访问)
|
||||||
|
public static final String BASE_URL = "https://wxpay.winboll.cc";
|
||||||
|
|
||||||
|
// 统一下单接口路径(对应服务端的测试接口)
|
||||||
|
public static final String CREATE_ORDER_URL = BASE_URL + "/pay/createOrder";
|
||||||
|
|
||||||
|
// 订单查询接口路径
|
||||||
|
public static final String QUERY_ORDER_URL = BASE_URL + "/pay/queryOrder";
|
||||||
|
|
||||||
|
// ========== 固定支付配置 ==========
|
||||||
|
public static final String ORDER_BODY = "定额测试支付"; // 商品描述
|
||||||
|
public static final int TOTAL_FEE = 1; // 固定金额:1分(沙箱环境推荐)
|
||||||
|
public static final String TRADE_TYPE = "NATIVE"; // 支付类型:二维码
|
||||||
|
|
||||||
|
// ========== 轮询配置 ==========
|
||||||
|
public static final long POLL_INTERVAL = 10000; // 轮询间隔:10秒
|
||||||
|
public static final long POLL_TIMEOUT = 45000; // 轮询超时:45秒
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
package cc.winboll.studio.winboll.activities;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||||
|
import cc.winboll.studio.winboll.R;
|
||||||
|
import cc.winboll.studio.winboll.WxPayConfig;
|
||||||
|
import cc.winboll.studio.winboll.utils.SpecUtil;
|
||||||
|
import cc.winboll.studio.winboll.utils.WxPayApi;
|
||||||
|
import cc.winboll.studio.winboll.utils.ZXingUtils;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主界面:生成二维码 + 轮询查询支付结果
|
||||||
|
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2026/01/07
|
||||||
|
*/
|
||||||
|
public class WXPayActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||||
|
|
||||||
|
private static final String TAG = "WXPayActivity";
|
||||||
|
|
||||||
|
// Handler消息标识
|
||||||
|
private static final int MSG_POLL_TIMEOUT = 1001;
|
||||||
|
private static final int MSG_POLL_SUCCESS = 1002;
|
||||||
|
private static final int MSG_POLL_FAILED = 1003;
|
||||||
|
|
||||||
|
private ImageView mIvQrCode;
|
||||||
|
private TextView mTvOrderNo;
|
||||||
|
private TextView mTvPayStatus;
|
||||||
|
private Button mBtnCreateOrder;
|
||||||
|
|
||||||
|
private String mOutTradeNo; // 商户订单号
|
||||||
|
private Timer mPollTimer; // 轮询定时器
|
||||||
|
private long mPollStartTime; // 轮询开始时间
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Activity getActivity() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTag() {
|
||||||
|
return TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Handler mPollHandler = new Handler() {
|
||||||
|
@Override
|
||||||
|
public void handleMessage(Message msg) {
|
||||||
|
super.handleMessage(msg);
|
||||||
|
switch (msg.what) {
|
||||||
|
case MSG_POLL_TIMEOUT:
|
||||||
|
stopPoll();
|
||||||
|
mTvPayStatus.setText("支付状态:轮询超时");
|
||||||
|
mTvPayStatus.setTextColor(getResources().getColor(android.R.color.darker_gray));
|
||||||
|
Toast.makeText(WXPayActivity.this, "轮询超时,请手动查询", Toast.LENGTH_SHORT).show();
|
||||||
|
break;
|
||||||
|
case MSG_POLL_SUCCESS:
|
||||||
|
boolean isPaySuccess = (boolean) msg.obj;
|
||||||
|
String tradeState = (String) msg.getData().getString("tradeState");
|
||||||
|
stopPoll();
|
||||||
|
if (isPaySuccess) {
|
||||||
|
mTvPayStatus.setText("支付状态:支付成功 ✅");
|
||||||
|
mTvPayStatus.setTextColor(getResources().getColor(android.R.color.holo_green_dark));
|
||||||
|
Toast.makeText(WXPayActivity.this, "支付成功!", Toast.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
mTvPayStatus.setText("支付状态:" + tradeState);
|
||||||
|
mTvPayStatus.setTextColor(getResources().getColor(android.R.color.holo_red_dark));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MSG_POLL_FAILED:
|
||||||
|
String errorMsg = (String) msg.obj;
|
||||||
|
mTvPayStatus.setText("查询失败:" + errorMsg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_wxpay);
|
||||||
|
|
||||||
|
initView();
|
||||||
|
initListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initView() {
|
||||||
|
mIvQrCode = findViewById(R.id.iv_qrcode);
|
||||||
|
mTvOrderNo = findViewById(R.id.tv_order_no);
|
||||||
|
mTvPayStatus = findViewById(R.id.tv_pay_status);
|
||||||
|
mBtnCreateOrder = findViewById(R.id.btn_create_order);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initListener() {
|
||||||
|
mBtnCreateOrder.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
createOrder();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一下单,生成二维码
|
||||||
|
*/
|
||||||
|
private void createOrder() {
|
||||||
|
mBtnCreateOrder.setEnabled(false);
|
||||||
|
mTvPayStatus.setText("支付状态:生成订单中...");
|
||||||
|
mIvQrCode.setImageBitmap(null);
|
||||||
|
|
||||||
|
WxPayApi.createOrder(new WxPayApi.OnCreateOrderCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(String outTradeNo, String codeUrl) {
|
||||||
|
mOutTradeNo = outTradeNo;
|
||||||
|
mTvOrderNo.setText("商户订单号:" + outTradeNo);
|
||||||
|
mTvPayStatus.setText("支付状态:未支付,请扫码");
|
||||||
|
|
||||||
|
// 生成二维码
|
||||||
|
Bitmap qrCodeBitmap = ZXingUtils.createQRCodeBitmap(codeUrl, 250, 250);
|
||||||
|
if (qrCodeBitmap != null) {
|
||||||
|
mIvQrCode.setImageBitmap(qrCodeBitmap);
|
||||||
|
// 开始轮询查询支付结果
|
||||||
|
startPoll();
|
||||||
|
} else {
|
||||||
|
mTvPayStatus.setText("支付状态:生成二维码失败");
|
||||||
|
mBtnCreateOrder.setEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(String errorMsg) {
|
||||||
|
SpecUtil.WWSpecLogError(TAG, "统一下单失败:" + errorMsg);
|
||||||
|
mTvPayStatus.setText("生成订单失败:" + errorMsg);
|
||||||
|
mBtnCreateOrder.setEnabled(true);
|
||||||
|
Toast.makeText(WXPayActivity.this, errorMsg, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始轮询查询支付结果
|
||||||
|
*/
|
||||||
|
private void startPoll() {
|
||||||
|
stopPoll(); // 先停止之前的轮询
|
||||||
|
mPollStartTime = System.currentTimeMillis();
|
||||||
|
mPollTimer = new Timer();
|
||||||
|
mPollTimer.scheduleAtFixedRate(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// 检查是否超时
|
||||||
|
long elapsedTime = System.currentTimeMillis() - mPollStartTime;
|
||||||
|
if (elapsedTime >= WxPayConfig.POLL_TIMEOUT) {
|
||||||
|
mPollHandler.sendEmptyMessage(MSG_POLL_TIMEOUT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询订单状态
|
||||||
|
WxPayApi.queryOrder(mOutTradeNo, new WxPayApi.OnQueryOrderCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(boolean isPaySuccess, String tradeState) {
|
||||||
|
Message msg = Message.obtain();
|
||||||
|
msg.what = MSG_POLL_SUCCESS;
|
||||||
|
msg.obj = isPaySuccess;
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putString("tradeState", tradeState);
|
||||||
|
msg.setData(bundle);
|
||||||
|
mPollHandler.sendMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(String errorMsg) {
|
||||||
|
Message msg = Message.obtain();
|
||||||
|
msg.what = MSG_POLL_FAILED;
|
||||||
|
msg.obj = errorMsg;
|
||||||
|
mPollHandler.sendMessage(msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 0, WxPayConfig.POLL_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止轮询
|
||||||
|
*/
|
||||||
|
private void stopPoll() {
|
||||||
|
if (mPollTimer != null) {
|
||||||
|
mPollTimer.cancel();
|
||||||
|
mPollTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
stopPoll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
package cc.winboll.studio.winboll.unittest;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import cc.winboll.studio.winboll.R;
|
||||||
|
import cc.winboll.studio.winboll.activities.WinBoLLActivity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2026/01/03 10:52
|
||||||
|
* @Describe 企业微信SDK接口测试(基础调试版)
|
||||||
|
* 包含:SDK初始化、基础接口调用、日志输出、主线程回调处理
|
||||||
|
*/
|
||||||
|
public class TestWeWorkSpecSDK extends WinBoLLActivity implements IWinBoLLActivity, View.OnClickListener {
|
||||||
|
|
||||||
|
public static final String TAG = "TestWeWorkSpecSDK";
|
||||||
|
|
||||||
|
// ------------------- 企业微信SDK配置常量(需替换为实际项目参数) -------------------
|
||||||
|
// 企业微信 CorpID(从企业微信管理后台获取)
|
||||||
|
private static final String CORP_ID = "wwb37c73f34c722852";
|
||||||
|
// 应用 AgentID(从企业微信应用管理后台获取)
|
||||||
|
private static final String AGENT_ID = "your_agent_id_here";
|
||||||
|
// 应用 Secret(从企业微信应用管理后台获取,注意保密)
|
||||||
|
private static final String APP_SECRET = "your_app_secret_here";
|
||||||
|
|
||||||
|
// ------------------- Handler消息标识(主线程处理SDK回调) -------------------
|
||||||
|
private static final int MSG_SDK_INIT_SUCCESS = 1001;
|
||||||
|
private static final int MSG_SDK_INIT_FAILED = 1002;
|
||||||
|
private static final int MSG_GET_CORP_INFO_SUCCESS = 1003;
|
||||||
|
private static final int MSG_GET_CORP_INFO_FAILED = 1004;
|
||||||
|
|
||||||
|
// ------------------- 控件声明 -------------------
|
||||||
|
private Button mBtnInitSDK;
|
||||||
|
private Button mBtnGetCorpInfo;
|
||||||
|
private Button mBtnCheckAuth;
|
||||||
|
|
||||||
|
// ------------------- 主线程Handler(处理SDK异步回调) -------------------
|
||||||
|
private Handler mWeWorkHandler;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Activity getActivity() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTag() {
|
||||||
|
return TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_test_weworkspecsdk);
|
||||||
|
|
||||||
|
// 初始化控件
|
||||||
|
initViews();
|
||||||
|
// 绑定点击事件
|
||||||
|
initEvents();
|
||||||
|
// 初始化Handler(主线程处理回调,更新UI)
|
||||||
|
initHandler();
|
||||||
|
// 初始化SDK(可选:启动时自动初始化,或点击按钮初始化)
|
||||||
|
// initWeWorkSDK();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化控件(Java 7 显式绑定)
|
||||||
|
*/
|
||||||
|
private void initViews() {
|
||||||
|
mBtnInitSDK = (Button) findViewById(R.id.btn_init_sdk);
|
||||||
|
mBtnGetCorpInfo = (Button) findViewById(R.id.btn_get_corp_info);
|
||||||
|
mBtnCheckAuth = (Button) findViewById(R.id.btn_check_auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定点击事件(Java 7 匿名内部类)
|
||||||
|
*/
|
||||||
|
private void initEvents() {
|
||||||
|
mBtnInitSDK.setOnClickListener(this);
|
||||||
|
mBtnGetCorpInfo.setOnClickListener(this);
|
||||||
|
mBtnCheckAuth.setOnClickListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化主线程Handler(处理SDK异步回调,安全更新UI)
|
||||||
|
*/
|
||||||
|
private void initHandler() {
|
||||||
|
mWeWorkHandler = new Handler() {
|
||||||
|
@Override
|
||||||
|
public void handleMessage(Message msg) {
|
||||||
|
super.handleMessage(msg);
|
||||||
|
switch (msg.what) {
|
||||||
|
case MSG_SDK_INIT_SUCCESS:
|
||||||
|
showToast("企业微信SDK初始化成功");
|
||||||
|
LogUtils.d(TAG, "SDK初始化成功");
|
||||||
|
break;
|
||||||
|
case MSG_SDK_INIT_FAILED:
|
||||||
|
String initError = (String) msg.obj;
|
||||||
|
showToast("SDK初始化失败:" + initError);
|
||||||
|
LogUtils.e(TAG, "SDK初始化失败:" + initError);
|
||||||
|
break;
|
||||||
|
case MSG_GET_CORP_INFO_SUCCESS:
|
||||||
|
String corpInfo = (String) msg.obj;
|
||||||
|
showToast("获取企业信息成功");
|
||||||
|
LogUtils.d(TAG, "企业信息:" + corpInfo);
|
||||||
|
break;
|
||||||
|
case MSG_GET_CORP_INFO_FAILED:
|
||||||
|
String corpError = (String) msg.obj;
|
||||||
|
showToast("获取企业信息失败:" + corpError);
|
||||||
|
LogUtils.e(TAG, "获取企业信息失败:" + corpError);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------- 企业微信SDK核心接口调用 -------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化企业微信SDK(异步操作,通过Handler回调结果)
|
||||||
|
*/
|
||||||
|
private void initWeWorkSDK() {
|
||||||
|
showToast("开始初始化企业微信SDK...");
|
||||||
|
// 模拟SDK异步初始化(实际项目中替换为企业微信SDK的真实初始化接口)
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
// 真实SDK初始化逻辑示例:
|
||||||
|
// WeWorkSDK.init(TestWeWorkSpecSDK.this, CORP_ID, AGENT_ID, new WeWorkSDKCallback() {
|
||||||
|
// @Override
|
||||||
|
// public void onSuccess() {
|
||||||
|
// mWeWorkHandler.sendEmptyMessage(MSG_SDK_INIT_SUCCESS);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @Override
|
||||||
|
// public void onFailure(String errorMsg) {
|
||||||
|
// Message msg = Message.obtain();
|
||||||
|
// msg.what = MSG_SDK_INIT_FAILED;
|
||||||
|
// msg.obj = errorMsg;
|
||||||
|
// mWeWorkHandler.sendMessage(msg);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// 调试模拟:休眠1秒,模拟异步初始化
|
||||||
|
Thread.sleep(1000);
|
||||||
|
// 模拟初始化成功(如需测试失败,替换为发送MSG_SDK_INIT_FAILED)
|
||||||
|
mWeWorkHandler.sendEmptyMessage(MSG_SDK_INIT_SUCCESS);
|
||||||
|
// 模拟初始化失败
|
||||||
|
// Message msg = Message.obtain();
|
||||||
|
// msg.what = MSG_SDK_INIT_FAILED;
|
||||||
|
// msg.obj = "CorpID或AgentID错误";
|
||||||
|
// mWeWorkHandler.sendMessage(msg);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
Message msg = Message.obtain();
|
||||||
|
msg.what = MSG_SDK_INIT_FAILED;
|
||||||
|
msg.obj = "线程中断:" + e.getMessage();
|
||||||
|
mWeWorkHandler.sendMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取企业基本信息(异步操作,需先初始化SDK)
|
||||||
|
*/
|
||||||
|
private void getCorpInfo() {
|
||||||
|
if (!isSDKInitialized()) {
|
||||||
|
showToast("请先初始化SDK");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showToast("开始获取企业信息...");
|
||||||
|
// 模拟SDK异步获取企业信息(实际项目中替换为真实接口)
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
// 真实SDK接口示例:
|
||||||
|
// WeWorkSDK.getCorpInfo(APP_SECRET, new CorpInfoCallback() {
|
||||||
|
// @Override
|
||||||
|
// public void onSuccess(CorpInfo info) {
|
||||||
|
// Message msg = Message.obtain();
|
||||||
|
// msg.what = MSG_GET_CORP_INFO_SUCCESS;
|
||||||
|
// msg.obj = "企业名称:" + info.getCorpName() + ",企业ID:" + info.getCorpId();
|
||||||
|
// mWeWorkHandler.sendMessage(msg);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @Override
|
||||||
|
// public void onFailure(String errorMsg) {
|
||||||
|
// Message msg = Message.obtain();
|
||||||
|
// msg.what = MSG_GET_CORP_INFO_FAILED;
|
||||||
|
// msg.obj = errorMsg;
|
||||||
|
// mWeWorkHandler.sendMessage(msg);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// 调试模拟:休眠1秒,模拟异步获取
|
||||||
|
Thread.sleep(1000);
|
||||||
|
// 模拟获取成功
|
||||||
|
Message successMsg = Message.obtain();
|
||||||
|
successMsg.what = MSG_GET_CORP_INFO_SUCCESS;
|
||||||
|
successMsg.obj = "企业名称:WinBoLL Studio,企业ID:" + CORP_ID;
|
||||||
|
mWeWorkHandler.sendMessage(successMsg);
|
||||||
|
// 模拟获取失败
|
||||||
|
// Message failMsg = Message.obtain();
|
||||||
|
// failMsg.what = MSG_GET_CORP_INFO_FAILED;
|
||||||
|
// failMsg.obj = "AppSecret错误或权限不足";
|
||||||
|
// mWeWorkHandler.sendMessage(failMsg);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
Message msg = Message.obtain();
|
||||||
|
msg.what = MSG_GET_CORP_INFO_FAILED;
|
||||||
|
msg.obj = "线程中断:" + e.getMessage();
|
||||||
|
mWeWorkHandler.sendMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查当前用户是否已授权(同步操作,示例)
|
||||||
|
*/
|
||||||
|
private void checkAuthStatus() {
|
||||||
|
if (!isSDKInitialized()) {
|
||||||
|
showToast("请先初始化SDK");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 真实SDK接口示例:
|
||||||
|
// boolean isAuthorized = WeWorkSDK.isAuthorized();
|
||||||
|
// 调试模拟:默认返回true
|
||||||
|
boolean isAuthorized = true;
|
||||||
|
|
||||||
|
if (isAuthorized) {
|
||||||
|
showToast("用户已授权");
|
||||||
|
LogUtils.d(TAG, "当前用户已授权企业微信应用");
|
||||||
|
} else {
|
||||||
|
showToast("用户未授权,请先授权");
|
||||||
|
LogUtils.d(TAG, "当前用户未授权企业微信应用");
|
||||||
|
// 真实项目中可调用授权接口:
|
||||||
|
// WeWorkSDK.requestAuth(TestWeWorkSpecSDK.this, new AuthCallback() {
|
||||||
|
// @Override
|
||||||
|
// public void onSuccess(String code) {
|
||||||
|
// showToast("授权成功,code:" + code);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @Override
|
||||||
|
// public void onFailure(String errorMsg) {
|
||||||
|
// showToast("授权失败:" + errorMsg);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------- 工具方法 -------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查SDK是否已初始化(模拟方法,实际项目中替换为SDK的真实状态检查)
|
||||||
|
*/
|
||||||
|
private boolean isSDKInitialized() {
|
||||||
|
// 真实SDK可通过静态方法检查状态:
|
||||||
|
// return WeWorkSDK.isInitialized();
|
||||||
|
// 调试模拟:假设Handler不为空即表示已初始化
|
||||||
|
return mWeWorkHandler != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示Toast提示(Java 7 简化封装)
|
||||||
|
*/
|
||||||
|
private void showToast(String msg) {
|
||||||
|
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------- 点击事件处理 -------------------
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
int id = v.getId();
|
||||||
|
if (id == R.id.btn_init_sdk) {
|
||||||
|
initWeWorkSDK();
|
||||||
|
} else if (id == R.id.btn_get_corp_info) {
|
||||||
|
getCorpInfo();
|
||||||
|
} else if (id == R.id.btn_check_auth) {
|
||||||
|
checkAuthStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------- 生命周期管理 -------------------
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
// 释放Handler资源,避免内存泄漏
|
||||||
|
if (mWeWorkHandler != null) {
|
||||||
|
mWeWorkHandler.removeCallbacksAndMessages(null);
|
||||||
|
mWeWorkHandler = null;
|
||||||
|
}
|
||||||
|
// 真实SDK需调用销毁方法:
|
||||||
|
// WeWorkSDK.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package cc.winboll.studio.winboll.utils;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import okhttp3.Call;
|
||||||
|
import okhttp3.Callback;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OkHttp网络请求工具类
|
||||||
|
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2026/01/07
|
||||||
|
*/
|
||||||
|
public class OkHttpUtil {
|
||||||
|
|
||||||
|
private static OkHttpClient sOkHttpClient;
|
||||||
|
private static Handler sMainHandler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
static {
|
||||||
|
sOkHttpClient = new OkHttpClient.Builder()
|
||||||
|
.connectTimeout(10, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(10, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(10, TimeUnit.SECONDS)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET请求
|
||||||
|
* @param url 请求地址
|
||||||
|
* @param callback 回调
|
||||||
|
*/
|
||||||
|
public static void get(String url, final OnResultCallback callback) {
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.get()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
sOkHttpClient.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call call, final IOException e) {
|
||||||
|
sMainHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onFailure(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call call, final Response response) throws IOException {
|
||||||
|
final String result = response.body().string();
|
||||||
|
sMainHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (callback != null) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
callback.onSuccess(result);
|
||||||
|
} else {
|
||||||
|
callback.onFailure("请求失败:" + response.code());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回调接口
|
||||||
|
*/
|
||||||
|
public interface OnResultCallback {
|
||||||
|
void onSuccess(String result);
|
||||||
|
void onFailure(String errorMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package cc.winboll.studio.winboll.utils;
|
||||||
|
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志工具类(适配项目规范)
|
||||||
|
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2026/01/07
|
||||||
|
*/
|
||||||
|
public class SpecUtil {
|
||||||
|
|
||||||
|
private static final boolean isDebug = true;
|
||||||
|
|
||||||
|
public static void WWSpecLogInfo(String tag, String msg) {
|
||||||
|
if (isDebug) {
|
||||||
|
LogUtils.i(tag, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void WWSpecLogError(String tag, String msg) {
|
||||||
|
if (isDebug) {
|
||||||
|
LogUtils.e(tag, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package cc.winboll.studio.winboll.utils;
|
||||||
|
|
||||||
|
import cc.winboll.studio.winboll.WxPayConfig;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信支付服务端接口封装
|
||||||
|
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2026/01/07
|
||||||
|
*/
|
||||||
|
public class WxPayApi {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一下单(生成二维码)
|
||||||
|
* @param callback 回调
|
||||||
|
*/
|
||||||
|
public static void createOrder(final OnCreateOrderCallback callback) {
|
||||||
|
// 拼接请求参数(服务端测试接口需支持GET传参,若为POST需修改为表单/JSON)
|
||||||
|
String url = WxPayConfig.CREATE_ORDER_URL +
|
||||||
|
"?body=" + WxPayConfig.ORDER_BODY +
|
||||||
|
"&totalFee=" + WxPayConfig.TOTAL_FEE +
|
||||||
|
"&tradeType=" + WxPayConfig.TRADE_TYPE;
|
||||||
|
|
||||||
|
OkHttpUtil.get(url, new OkHttpUtil.OnResultCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(String result) {
|
||||||
|
try {
|
||||||
|
JSONObject jsonObject = JSONObject.parseObject(result);
|
||||||
|
String outTradeNo = jsonObject.getString("out_trade_no");
|
||||||
|
String codeUrl = jsonObject.getString("code_url");
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onSuccess(outTradeNo, codeUrl);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onFailure("解析统一下单结果失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(String errorMsg) {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onFailure("统一下单请求失败:" + errorMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单查询
|
||||||
|
* @param outTradeNo 商户订单号
|
||||||
|
* @param callback 回调
|
||||||
|
*/
|
||||||
|
public static void queryOrder(String outTradeNo, final OnQueryOrderCallback callback) {
|
||||||
|
String url = WxPayConfig.QUERY_ORDER_URL + "?outTradeNo=" + outTradeNo;
|
||||||
|
|
||||||
|
OkHttpUtil.get(url, new OkHttpUtil.OnResultCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(String result) {
|
||||||
|
try {
|
||||||
|
JSONObject jsonObject = JSONObject.parseObject(result);
|
||||||
|
String tradeState = jsonObject.getString("trade_state");
|
||||||
|
boolean isSuccess = "SUCCESS".equals(tradeState);
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onSuccess(isSuccess, tradeState);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onFailure("解析订单查询结果失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(String errorMsg) {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onFailure("订单查询请求失败:" + errorMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一下单回调接口
|
||||||
|
*/
|
||||||
|
public interface OnCreateOrderCallback {
|
||||||
|
void onSuccess(String outTradeNo, String codeUrl);
|
||||||
|
void onFailure(String errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单查询回调接口
|
||||||
|
*/
|
||||||
|
public interface OnQueryOrderCallback {
|
||||||
|
void onSuccess(boolean isPaySuccess, String tradeState);
|
||||||
|
void onFailure(String errorMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package cc.winboll.studio.winboll.utils;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import com.google.zxing.BarcodeFormat;
|
||||||
|
import com.google.zxing.EncodeHintType;
|
||||||
|
import com.google.zxing.WriterException;
|
||||||
|
import com.google.zxing.common.BitMatrix;
|
||||||
|
import com.google.zxing.qrcode.QRCodeWriter;
|
||||||
|
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
|
||||||
|
import com.journeyapps.barcodescanner.BarcodeEncoder;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ZXing二维码生成工具类
|
||||||
|
* 依赖:com.google.zxing:core:3.4.1 + com.journeyapps:zxing-android-embedded:3.6.0
|
||||||
|
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2026/01/07
|
||||||
|
*/
|
||||||
|
public class ZXingUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成二维码Bitmap(核心方法,使用journeyapps工具类)
|
||||||
|
* @param content 内容(如微信支付的code_url)
|
||||||
|
* @param width 二维码宽度(px)
|
||||||
|
* @param height 二维码高度(px)
|
||||||
|
* @return 二维码Bitmap,失败返回null
|
||||||
|
*/
|
||||||
|
public static Bitmap createQRCodeBitmap(String content, int width, int height) {
|
||||||
|
// 1. 入参合法性校验
|
||||||
|
if (content == null || content.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 配置二维码参数
|
||||||
|
Map<EncodeHintType, Object> hints = new HashMap<>();
|
||||||
|
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); // 字符编码
|
||||||
|
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); // 高容错级别(H级可容忍30%遮挡)
|
||||||
|
hints.put(EncodeHintType.MARGIN, 1); // 边距(值越小,二维码越紧凑,建议1-4)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 3. 生成BitMatrix(二维码矩阵)
|
||||||
|
QRCodeWriter qrCodeWriter = new QRCodeWriter();
|
||||||
|
BitMatrix bitMatrix = qrCodeWriter.encode(
|
||||||
|
content,
|
||||||
|
BarcodeFormat.QR_CODE,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
hints
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. 转换BitMatrix为Bitmap(关键:使用journeyapps的BarcodeEncoder)
|
||||||
|
BarcodeEncoder barcodeEncoder = new BarcodeEncoder();
|
||||||
|
return barcodeEncoder.createBitmap(bitMatrix);
|
||||||
|
|
||||||
|
} catch (WriterException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重载方法:生成正方形二维码(宽度=高度)
|
||||||
|
* @param content 内容
|
||||||
|
* @param size 二维码边长(px)
|
||||||
|
* @return 二维码Bitmap
|
||||||
|
*/
|
||||||
|
public static Bitmap createQRCodeBitmap(String content, int size) {
|
||||||
|
return createQRCodeBitmap(content, size, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
210
winboll/src/main/java/com/tencent/wework/SpecCallbackSDK.java
Normal file
210
winboll/src/main/java/com/tencent/wework/SpecCallbackSDK.java
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
package com.tencent.wework;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.HashMap;;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @warning: 1. 不要修改成员变量名,native方法内有反射调用
|
||||||
|
* 2. 调用本地方法需保持包结构,本工具需放在包com.tencent.wework内
|
||||||
|
* 3. 不允许继承,类名和函数名均不可修改,会影响本地方法的引用,详见:javah生成本地方法头文件
|
||||||
|
*/
|
||||||
|
public final class SpecCallbackSDK {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 调用本地方法后实例化的对象指针
|
||||||
|
*/
|
||||||
|
private long specCallbackSDKptr = 0;
|
||||||
|
|
||||||
|
public long GetPtr() { return specCallbackSDKptr; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 回包的headers
|
||||||
|
*/
|
||||||
|
private Map<String, String> responseHeaders;
|
||||||
|
|
||||||
|
public Map<String, String> GetResponseHeaders() { return responseHeaders; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 回包的加密后的body
|
||||||
|
*/
|
||||||
|
private String responseBody;
|
||||||
|
|
||||||
|
public String GetResponseBody() { return responseBody; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 每个请求构造一个SpecCallbackSDK示例,
|
||||||
|
* SpecCallbackSDK仅持有headers和body的引用,
|
||||||
|
* 因此需保证headers和body的生存期比SpecCallbackSDK长
|
||||||
|
* @param method: 请求方法GET/POST
|
||||||
|
* @param headers: 请求header
|
||||||
|
* @param body: 请求body
|
||||||
|
* @example:
|
||||||
|
* SpecCallbackSDK sdk = new SpecCallbackSDK(method, headers, body);
|
||||||
|
* if (sdk.IsOk()) {
|
||||||
|
* String corpid = sdk.GetCorpId();
|
||||||
|
* String agentid = sdk.GetAgentId();
|
||||||
|
* String call_type = sdk.GetCallType();
|
||||||
|
* String data = sdk.GetData();
|
||||||
|
* //do something...
|
||||||
|
* }
|
||||||
|
* String response = ...;
|
||||||
|
* sdk.BuildResponseHeaderBody(response);
|
||||||
|
* Map<String, String> responseHeaders = sdk.GetResponseHeaders();
|
||||||
|
* String body = sdk.GetResponseBody();
|
||||||
|
* //do response
|
||||||
|
*
|
||||||
|
* @return errorcode 示例如下:
|
||||||
|
* -920001: 未设置请求方法
|
||||||
|
* -920002: 未设置请求header
|
||||||
|
* -920003: 未设置请求body
|
||||||
|
* */
|
||||||
|
public SpecCallbackSDK(String method, Map<String, String> headers, String body) {
|
||||||
|
try {
|
||||||
|
specCallbackSDKptr = NewCallbackSDK(method, headers, body);
|
||||||
|
} catch (Exception e) {
|
||||||
|
SpecUtil.WWSpecLogError("SpecCallbackSDK exception caught", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private native long NewCallbackSDK(String method, Map<String, String> headers, String body);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @usage 在Java对象的内存回收前析构C++对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void finalize() throws Throwable {
|
||||||
|
DeleteCPPInstance(specCallbackSDKptr);
|
||||||
|
super.finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private native void DeleteCPPInstance(long specCallbackSDKptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 判断构造函数中传入的请求是否解析成功
|
||||||
|
* @return: 成功与否
|
||||||
|
* */
|
||||||
|
public boolean IsOk() {
|
||||||
|
return IsOk(specCallbackSDKptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native boolean IsOk(long specCallbackSDKptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 获取请求的企业
|
||||||
|
* @require: 仅当IsOk() == true可调用
|
||||||
|
* @return: corpid
|
||||||
|
* */
|
||||||
|
public String GetCorpId() {
|
||||||
|
return GetCorpId(specCallbackSDKptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native String GetCorpId(long specCallbackSDKptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 获取请求的应用
|
||||||
|
* @require: 仅当IsOk() == true可调用
|
||||||
|
* @return: agentid
|
||||||
|
* */
|
||||||
|
public long GetAgentId() {
|
||||||
|
return GetAgentId(specCallbackSDKptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native long GetAgentId(long specCallbackSDKptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 获取请求的类型
|
||||||
|
* @require: 仅当IsOk() == true可调用
|
||||||
|
* @return: 1 - 来自[应用调用专区]的请求
|
||||||
|
* 2 - 来自企业微信的回调事件
|
||||||
|
* */
|
||||||
|
public long GetCallType() {
|
||||||
|
return GetCallType(specCallbackSDKptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native long GetCallType(long specCallbackSDKptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 获取请求数据
|
||||||
|
* @require: 仅当IsOk() == true可调用
|
||||||
|
* @return: 请求数据,根据call_type可能是:
|
||||||
|
* - 企业微信回调事件
|
||||||
|
* - [应用调用专区]接口中的request_data
|
||||||
|
* */
|
||||||
|
public String GetData() {
|
||||||
|
return GetData(specCallbackSDKptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native String GetData(long specCallbackSDKptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 是否异步请求
|
||||||
|
* @require: 仅当IsOk() == true可调用
|
||||||
|
* @return: 是否异步请求
|
||||||
|
* */
|
||||||
|
public boolean GetIsAsync() {
|
||||||
|
return GetIsAsync(specCallbackSDKptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native boolean GetIsAsync(long specCallbackSDKptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 获取请求的job_info,
|
||||||
|
* @require: 仅当IsOk() == true可调用
|
||||||
|
* @return: job_info,无需理解内容,
|
||||||
|
* 在同一个请求上下文中使用SpecSDK的时候传入
|
||||||
|
* */
|
||||||
|
public String GetJobInfo() {
|
||||||
|
return GetJobInfo(specCallbackSDKptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native String GetJobInfo(long specCallbackSDKptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 获取请求的ability_id,[应用调用专区]接口时指定
|
||||||
|
* @require: 仅当IsOk() == true可调用
|
||||||
|
* @return: ability_id
|
||||||
|
* */
|
||||||
|
public String GetAbilityId() {
|
||||||
|
return GetAbilityId(specCallbackSDKptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native String GetAbilityId(long specCallbackSDKptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 获取请求的notify_id,用于[应用同步调用专区程序]接口
|
||||||
|
* @require: 仅当IsOk() == true可调用
|
||||||
|
* @return: notify_id
|
||||||
|
* */
|
||||||
|
public String GetNotifyId() {
|
||||||
|
return GetNotifyId(specCallbackSDKptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native String GetNotifyId(long specCallbackSDKptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 对返回包计算签名&加密
|
||||||
|
* @param response: 待加密的回包明文.如果IsOk()==false,传入空串即可
|
||||||
|
* @note 本接口的执行问题可查看日志
|
||||||
|
* */
|
||||||
|
public void BuildResponseHeaderBody(String response) {
|
||||||
|
try {
|
||||||
|
responseHeaders = new HashMap<String, String>();
|
||||||
|
responseBody = "";
|
||||||
|
BuildResponseHeaderBody(specCallbackSDKptr, response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
SpecUtil.WWSpecLogError("SpecCallbackSDK exception caught", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private native void BuildResponseHeaderBody(long specCallbackSDKptr, String response);
|
||||||
|
|
||||||
|
// 静态代码块内还无法调用native日志函数,这里的日志在管理系统无法查询
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
Class.forName("com.tencent.wework.SpecUtil");
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
163
winboll/src/main/java/com/tencent/wework/SpecSDK.java
Normal file
163
winboll/src/main/java/com/tencent/wework/SpecSDK.java
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package com.tencent.wework;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @warning: 1. 不要修改成员变量名,native方法内有反射调用
|
||||||
|
* 2. 调用本地方法需保持包结构,本工具需放在包com.tencent.wework内
|
||||||
|
* 3. 不允许继承,类名和函数名均不可修改,会影响本地方法的引用,详见:javah生成本地方法头文件
|
||||||
|
*/
|
||||||
|
public final class SpecSDK {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 调用本地方法后实例化的对象指针
|
||||||
|
*/
|
||||||
|
private long specSDKptr = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @usage invoke的请求
|
||||||
|
* @example "{\"limit\":1}
|
||||||
|
*/
|
||||||
|
private String request;
|
||||||
|
|
||||||
|
public void SetRequest(String request) {
|
||||||
|
this.request = request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @usage 访问上一次invoke的结果
|
||||||
|
*/
|
||||||
|
private String response;
|
||||||
|
|
||||||
|
public String GetResponse() {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param corpid: 企业corpid,必选参数
|
||||||
|
* @param agentid: 应用id,必选参数
|
||||||
|
* @param ability_id: 能力ID,可选参数
|
||||||
|
* @param job_info: job_info,可选参数
|
||||||
|
* */
|
||||||
|
public SpecSDK(String corpId, long agentId) {
|
||||||
|
specSDKptr = NewSDK1(corpId, agentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native long NewSDK1(String corpId, long agentId);
|
||||||
|
|
||||||
|
public SpecSDK(String corpId, long agentId, String abilityId) {
|
||||||
|
specSDKptr = NewSDK2(corpId, agentId, abilityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native long NewSDK2(String corpId, long agentId, String abilityId);
|
||||||
|
|
||||||
|
public SpecSDK(String corpId, long agentId, String abilityId, String jobInfo) {
|
||||||
|
specSDKptr = NewSDK3(corpId, agentId, abilityId, jobInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native long NewSDK3(String corpId, long agentId, String abilityId, String jobInfo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 使用callback的请求来初始化
|
||||||
|
* @param callback_sdk: 要求IsOk()==true
|
||||||
|
* @return C++内部指针,创建失败时指针仍为0,并输出错误日志
|
||||||
|
* */
|
||||||
|
public SpecSDK(SpecCallbackSDK callbackSDK) {
|
||||||
|
specSDKptr = NewSDK4(callbackSDK.GetPtr());
|
||||||
|
}
|
||||||
|
|
||||||
|
private native long NewSDK4(long callbackSDK);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @usage 在Java对象的内存回收前析构C++对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void finalize() throws Throwable {
|
||||||
|
DeleteCPPInstance(specSDKptr);
|
||||||
|
super.finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private native void DeleteCPPInstance(long specSDKptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 用于在专区内调用企业微信接口
|
||||||
|
* @param api_name 接口名
|
||||||
|
* @param request json格式的请求数据
|
||||||
|
* @param response json格式的返回数据
|
||||||
|
* @return errorcode 参考如下:
|
||||||
|
* 0: 成功
|
||||||
|
* -910001: SDK没有初始化
|
||||||
|
* -910002: 没有设置请求体
|
||||||
|
* -910003: 没有设置请求的API
|
||||||
|
* -910004: 在SDK成员内找不到成员"response",注意lib内有反射机制,不要修改成员变量名
|
||||||
|
* -910005: 使用未初始化的callback初始化SDK
|
||||||
|
* -910006: invoke调用失败,应检查日志查看具体原因
|
||||||
|
* -910007: 响应体为空
|
||||||
|
* @note 当返回0时,表示没有网络或请求协议层面或调用方法的失败,
|
||||||
|
* 调用方需继续检查response中的errcode字段确保业务层面的成功
|
||||||
|
*
|
||||||
|
* @usage 当前版本sdk支持的接口列表,每个接口的具体协议请查看企业微信文档:
|
||||||
|
* https://developer.work.weixin.qq.com/document/path/91201
|
||||||
|
*
|
||||||
|
* +--------------------------------+--------------------------------+
|
||||||
|
* |接口名 |描述 |
|
||||||
|
* |--------------------------------|--------------------------------|
|
||||||
|
* |program_async_job_call_back |上报异步任务结果 |
|
||||||
|
* |sync_msg |获取会话记录 |
|
||||||
|
* |get_group_chat |获取内部群信息 |
|
||||||
|
* |get_agree_status_single |获取单聊会话同意情况 |
|
||||||
|
* |get_agree_status_room |获取群聊会话同意情况 |
|
||||||
|
* |set_hide_sensitiveinfo_config |设置成员会话组件敏感信息隐藏配置|
|
||||||
|
* |get_hide_sensitiveinfo_config |获取成员会话组件敏感信息隐藏配置|
|
||||||
|
* |search_chat |会话名称搜索 |
|
||||||
|
* |search_msg |会话消息搜索 |
|
||||||
|
* |create_rule |新增关键词规则 |
|
||||||
|
* |get_rule_list |获取关键词列表 |
|
||||||
|
* |get_rule_detail |获取关键词规则详情 |
|
||||||
|
* |update_rule |修改关键词规则 |
|
||||||
|
* |delete_rule |删除关键词规则 |
|
||||||
|
* |get_hit_msg_list |获取命中关键词规则的会话记录 |
|
||||||
|
* |create_sentiment_task |创建情感分析任务 |
|
||||||
|
* |get_sentiment_result |获取情感分析结果 |
|
||||||
|
* |create_summary_task |创建摘要提取任务 |
|
||||||
|
* |get_summary_result |获取摘要提取结果 |
|
||||||
|
* |create_customer_tag_task |创建标签匹配任务 |
|
||||||
|
* |get_customer_tag_result |获取标签任务结果 |
|
||||||
|
* |create_recommend_dialog_task |创建话术推荐任务 |
|
||||||
|
* |get_recommend_dialog_result |获取话术推荐结果 |
|
||||||
|
* |create_private_task |创建自定义模型任务 |
|
||||||
|
* |get_private_task_result |获取自定义模型结果 |
|
||||||
|
* |(废弃)document_list |获取知识集列表 |
|
||||||
|
* |create_spam_task |会话反垃圾创建分析任务 |
|
||||||
|
* |get_spam_result |会话反垃圾获取任务结果 |
|
||||||
|
* |create_chatdata_export_job |创建会话内容导出任务 |
|
||||||
|
* |get_chatdata_export_job_status |获取会话内容导出任务结果 |
|
||||||
|
* |spec_notify_app |专区通知应用 |
|
||||||
|
* |create_program_task |创建自定义程序任务 |
|
||||||
|
* |get_program_task_result |获取自定义程序结果 |
|
||||||
|
* |knowledge_base_list |获取企业授权给应用的知识集列表 |
|
||||||
|
* |knowledge_base_create |创建知识集 |
|
||||||
|
* |knowledge_base_detail |获取知识集详情 |
|
||||||
|
* |knowledge_base_add_doc |添加知识集內容 |
|
||||||
|
* |knowledge_base_remove_doc |删除知识集內容 |
|
||||||
|
* |knowledge_base_modify_name |修改知识集名称 |
|
||||||
|
* |knowledge_base_delete |删除知识集 |
|
||||||
|
* |search_contact_or_customer |员工或者客户名称搜索 |
|
||||||
|
* |create_ww_model_task |创建企微通用模型任务 |
|
||||||
|
* |get_ww_model_result |获取企微通用模型结果 |
|
||||||
|
* |get_msg_list_by_page_id |page_id获取消息列表 |
|
||||||
|
* +-----------------------------------------------------------------+
|
||||||
|
* */
|
||||||
|
public int Invoke(String apiName) {
|
||||||
|
return Invoke(specSDKptr, apiName, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private native int Invoke(long sdk, String apiName, String request);
|
||||||
|
|
||||||
|
// 静态代码块内还无法调用native日志函数,这里的日志在管理系统无法查询
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
Class.forName("com.tencent.wework.SpecUtil");
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
171
winboll/src/main/java/com/tencent/wework/SpecUtil.java
Normal file
171
winboll/src/main/java/com/tencent/wework/SpecUtil.java
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
package com.tencent.wework;
|
||||||
|
|
||||||
|
//import java.lang.management.ManagementFactory;
|
||||||
|
//import java.lang.management.RuntimeMXBean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @warning: 1. 不要修改成员变量名,native方法内有反射调用
|
||||||
|
* 2. 调用本地方法需保持包结构,本工具需放在包com.tencent.wework内
|
||||||
|
* 3. 不允许继承,类名和函数名均不可修改,会影响本地方法的引用,详见:javah生成本地方法头文件
|
||||||
|
* 4. 使用其他工具打印的日志将无法被查询,如需使用SLF4j风格的日志或性能更好的日志框架,
|
||||||
|
* 请自行封装SpecUtil.SpecLog或SpecUtil.SpecLogNative方法
|
||||||
|
*
|
||||||
|
* @usage: 1. 获取SDK的版本号
|
||||||
|
* 2. 打印三个级别的日志
|
||||||
|
* 3. 开启调试模式
|
||||||
|
*/
|
||||||
|
public final class SpecUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description SDK版本号
|
||||||
|
* @usage 可用于校对不同SDK版本,或后续针对不同的SDK版本添加业务逻辑
|
||||||
|
*/
|
||||||
|
private static final String SDK_VERSION = "1.4.0";
|
||||||
|
|
||||||
|
public static String GetSDKVersion() {
|
||||||
|
return SDK_VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 正确的包名,SDK必须存放在"com.tencent.wework"下,否则会影响本地方法的调用
|
||||||
|
*/
|
||||||
|
private static final String EXPECTED_PACKAGE_NAME = "com.tencent.wework";
|
||||||
|
|
||||||
|
public static String GetExpectedPackageName() {
|
||||||
|
return EXPECTED_PACKAGE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String LINE_SEPERATOR = System.getProperty("line.separator");
|
||||||
|
|
||||||
|
public static void WWSpecLogInfo(String... args) {
|
||||||
|
SpecLog('I', args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void WWSpecLogError(String... args) {
|
||||||
|
SpecLog('E', args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void WWSpecLogDebug(String... args) {
|
||||||
|
SpecLog('D', args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void WWSpecLogInfoWithReqId(String reqId, String... args) {
|
||||||
|
SpecLogWithReqId(reqId, 'I', args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void WWSpecLogErrorWithReqId(String reqId, String... args) {
|
||||||
|
SpecLogWithReqId(reqId, 'E', args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void WWSpecLogDebugWithReqId(String reqId, String... args) {
|
||||||
|
SpecLogWithReqId(reqId, 'D', args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @usage 打印标准日志
|
||||||
|
* @note 只有使用SpecLog和SpecLogNative函数打印的日志能被调试平台查询,其他框架的日志仅能本地查看
|
||||||
|
* @param logLevel 日志级别,使用char传递,目前支持I——INFO、E——ERROR、D——DEBUG
|
||||||
|
* @param args 自定义参数
|
||||||
|
*/
|
||||||
|
public static void SpecLog(char logLevel, String... args) {
|
||||||
|
StackTraceElement element = Thread.currentThread().getStackTrace()[3];
|
||||||
|
SpecLogNative(
|
||||||
|
logLevel,
|
||||||
|
element.getFileName(),
|
||||||
|
element.getLineNumber(),
|
||||||
|
String.join(",", args).replace(LINE_SEPERATOR, " ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @usage 打印标准日志
|
||||||
|
* @note 只有使用SpecLog和SpecLogNative函数打印的日志能被调试平台查询,其他框架的日志仅能本地查看
|
||||||
|
* @param reqid 请求id
|
||||||
|
* @param logLevel 日志级别,使用char传递,目前支持I——INFO、E——ERROR、D——DEBUG
|
||||||
|
* @param args 自定义参数
|
||||||
|
*/
|
||||||
|
public static void SpecLogWithReqId(String reqId, char logLevel, String... args) {
|
||||||
|
StackTraceElement element = Thread.currentThread().getStackTrace()[3];
|
||||||
|
SpecLogNativeWithReqId(
|
||||||
|
reqId,
|
||||||
|
logLevel,
|
||||||
|
element.getFileName(),
|
||||||
|
element.getLineNumber(),
|
||||||
|
String.join(",", args).replace(LINE_SEPERATOR, " ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @usage 打印标准日志
|
||||||
|
* @note 只有使用SpecLog和SpecLogNative函数打印的日志能被调试平台查询,其他框架的日志仅能本地查看
|
||||||
|
* 如需SLF4J风格的接口或对日志性能有进一步需求,开发者可以自行封装该函数
|
||||||
|
* @param logLevel 日志级别,使用char传递,目前支持I——INFO、E——ERROR、D——DEBUG
|
||||||
|
* @param fileName 文件名(类名)
|
||||||
|
* @param lineNumber 行号
|
||||||
|
* @param argsString 自定义参数
|
||||||
|
*/
|
||||||
|
public static native void SpecLogNative(char logLevel, String fileName, int lineNumber, String argsString);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @usage 打印标准日志
|
||||||
|
* @note 只有使用SpecLog和SpecLogNative函数打印的日志能被调试平台查询,其他框架的日志仅能本地查看
|
||||||
|
* 如需SLF4J风格的接口或对日志性能有进一步需求,开发者可以自行封装该函数
|
||||||
|
* @param reqid 请求id
|
||||||
|
* @param logLevel 日志级别,使用char传递,目前支持I——INFO、E——ERROR、D——DEBUG
|
||||||
|
* @param fileName 文件名(类名)
|
||||||
|
* @param lineNumber 行号
|
||||||
|
* @param argsString 自定义参数
|
||||||
|
*/
|
||||||
|
public static native void SpecLogNativeWithReqId(String reqId, char logLevel, String fileName, int lineNumber, String argsString);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @usage 开启调试模式,进程级别开关
|
||||||
|
* @param debugToken 调试凭证,在管理端获取
|
||||||
|
* @param accessToken 应用access token
|
||||||
|
* @return 是否开启成功
|
||||||
|
*/
|
||||||
|
public static boolean SpecOpenDebugMode(String debugToken, String accessToken) {
|
||||||
|
return SpecOpenDebugModeNative(debugToken, accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native boolean SpecOpenDebugModeNative(String debugToken, String accessToken);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @usage 生成notify id。用户可调用本接口生成notify id,也可完全自定义生成
|
||||||
|
* @return 新的notify id,支持纳秒级隔离,内部异常时会输出日志并返回空串
|
||||||
|
* @note 1. 用户可先生成notify id,将其与回调数据关联存储后,再使用该notify id通知应用,
|
||||||
|
* 从而保证回调数据被请求时已存储完毕
|
||||||
|
*/
|
||||||
|
public static String GenerateNotifyId() {
|
||||||
|
return GenerateNotifyIdNative();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native String GenerateNotifyIdNative();
|
||||||
|
|
||||||
|
static {
|
||||||
|
// 检查包名
|
||||||
|
String packageName = SpecUtil.class.getPackage().getName();
|
||||||
|
if (!EXPECTED_PACKAGE_NAME.equals(packageName)) {
|
||||||
|
// 静态代码块内还无法调用native日志函数,这里的日志在管理系统无法查询
|
||||||
|
System.out.println("SpecUtil class must be in package com.tencent.wework");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载so库
|
||||||
|
try {
|
||||||
|
System.loadLibrary("WeWorkSpecSDK");
|
||||||
|
} catch (UnsatisfiedLinkError e) {
|
||||||
|
System.out.println("libWeWorkSpecSDK.so not found in java.library.path");
|
||||||
|
e.printStackTrace();
|
||||||
|
System.exit(1);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("unexpected exception: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
SpecUtil.WWSpecLogInfo("SDK init done", "packageName=" + packageName, "SDK_VERSION=" + SDK_VERSION);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
winboll/src/main/res/layout/activity_test_weworkspecsdk.xml
Normal file
30
winboll/src/main/res/layout/activity_test_weworkspecsdk.xml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="20dp"
|
||||||
|
android:gravity="center_horizontal">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_init_sdk"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="初始化企业微信SDK"
|
||||||
|
android:layout_marginBottom="10dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_get_corp_info"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="获取企业基本信息"
|
||||||
|
android:layout_marginBottom="10dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_check_auth"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="检查用户授权状态"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
43
winboll/src/main/res/layout/activity_wxpay.xml
Normal file
43
winboll/src/main/res/layout/activity_wxpay.xml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:padding="20dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="定额支付测试(0.01元)"
|
||||||
|
android:textSize="20sp"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_qrcode"
|
||||||
|
android:layout_width="250dp"
|
||||||
|
android:layout_height="250dp"
|
||||||
|
android:scaleType="fitXY"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_order_no"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="商户订单号:"
|
||||||
|
android:textSize="16sp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_pay_status"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="支付状态:未支付"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="@android:color/black"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_create_order"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="生成支付二维码"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
@@ -7,4 +7,7 @@
|
|||||||
<item
|
<item
|
||||||
android:id="@+id/item_settings"
|
android:id="@+id/item_settings"
|
||||||
android:title="Settings"/>
|
android:title="Settings"/>
|
||||||
|
<item
|
||||||
|
android:id="@+id/item_wxpayactivity"
|
||||||
|
android:title="WXPayActivity"/>
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
Reference in New Issue
Block a user