Compare commits

...

14 Commits

Author SHA1 Message Date
04fd7e74fa <contacts>APK 15.14.2 release Publish. 2026-01-06 20:25:41 +08:00
cdf51637d3 更新编译配置和基础类库。 2026-01-06 20:17:16 +08:00
4ae2133d0f Merge branch 'winboll' into contacts 2026-01-06 20:11:45 +08:00
cce4a643e7 编译环境参数调整 2026-01-06 14:08:30 +08:00
61cfa7f3ff 添加微信支付测试窗口 2026-01-04 17:05:12 +08:00
cb9ad1dc57 添加企业微信测试码 2026-01-03 11:16:35 +08:00
98c334f442 移除GitHub工作流配置文件 2026-01-03 10:19:43 +08:00
47c328cd25 <winboll>APK 15.11.8 release Publish. 2025-12-31 20:28:06 +08:00
7134d4e1c8 源码整理 2025-12-31 20:25:15 +08:00
84c6271310 <contacts>APK 15.14.1 release Publish. 2025-12-15 20:54:20 +08:00
a4ab864381 通话测试通过。 2025-12-15 20:51:56 +08:00
217a27cbcd 更新基础类库 2025-12-15 18:41:04 +08:00
778a1bc98e 添加应用版本号说明 2025-12-15 12:42:50 +08:00
561abd2398 20251214_204905_236 2025-12-14 20:49:11 +08:00
30 changed files with 2636 additions and 651 deletions

View File

@@ -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 文件)

View File

@@ -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

View File

@@ -64,6 +64,11 @@ android {
dimension "WinBoLLApp" dimension "WinBoLLApp"
} }
} }
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
// 应用包输出配置 // 应用包输出配置
// //

View File

@@ -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

View File

@@ -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) {

View File

@@ -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'])
} }

View File

@@ -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

View File

@@ -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 API26isDestroyed适配用
// ====================== 单例与成员变量区 ====================== // 单例与核心成员变量(按优先级排序)
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");
}
}
});
}
// ====================== 栈优化与工具方法 ======================
/**
* 清理栈中所有无效Activitynull/已销毁/已结束),优化小米机型内存占用
*/
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;
}
} }

View File

@@ -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;
} }
} }

View File

@@ -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;
} }
} }

View File

@@ -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;
}
} }

View File

@@ -336,11 +336,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
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"
@@ -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>

View File

@@ -19,11 +19,11 @@ def genVersionName(def versionName){
android { android {
// 1. compileSdkVersion必须 ≥ targetSdkVersion建议直接等于 targetSdkVersion30 // 关键:改为你已安装的 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'

View File

@@ -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

Binary file not shown.

View File

@@ -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>

View File

@@ -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();

View File

@@ -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秒
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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);
}
}
}

View 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();
}
}
}

View 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);
}
}

View 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>

View 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>

View File

@@ -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>