2026-05-13 15:54:02 +08:00
commit 47ace290e5
1181 changed files with 101801 additions and 0 deletions

101
.gitignore vendored Normal file
View File

@@ -0,0 +1,101 @@
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
*.jks
*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# 忽略 Lint 输出文件
lint-results.xml
lint-results.html
## 忽略 AndroidIDE 临时文件夹
.androidide
## WinBoLL 基础应用(避免上传敏感配置)
/winboll.properties
/local.properties
/settings.gradle
/gradle.properties

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "libjc/jcc/libs"]
path = libjc/jcc/libs
url = https://gitea.winboll.cc/Studio/APP_libjc_jcc_libs.git

18
.winboll/Readme.txt Normal file
View File

@@ -0,0 +1,18 @@
## WinBoLL 主机编译事项提醒
## 类库类型源码发布
# 类库发布使用以下面命令
git pull && bash .winboll/bashPublishLIBAddTag.sh <类库模块文件夹名称>
## 纯应用类型源码发布
# 应用发布使用以下命令
git pull && bash .winboll/bashPublishAPKAddTag.sh <应用模块文件夹名称>
## 编译时提问。Add Github Workflows Tag? (yes/No)
回答yes: 将会添加一个 GitHub 工作流标签
GitHub 仓库会执行以该标签为标志的编译工作流。
回答No(默认): 就忽略 GitHub 标签,忽略 GitHub 工作流调用。
## Github Workflows 工作流设置注意事项
应用名称改变时需要修改.github/workflows/android.yml文件设置
在第79行asset_name: 处有应用包名称设置。

View File

@@ -0,0 +1,32 @@
#!/usr/bin/bash
# 使用 `-z` 命令检查变量是否为空
if [ -z "$1" ] || [ -z "$2" ]; then
echo "Script parameter error: $0"
exit 2
fi
# 进入项目根目录
cd ${1}
echo -e "Work dir : \n"`pwd`
git config --global --add safe.directory "${1}"
echo "Current dir : "`pwd`
versionName=${2}
## 设置要检查的标签
tag="v"${versionName}
## 如果Git已经提交了所有代码就执行标签检查操作
if [[ -n $(git diff --stat) ]]
then
echo 'Source is no commit git completely, tag action cancel.'
exit 1
else
echo "Git status is clean."
if [ "$(git tag -l ${tag})" == "${tag}" ]; then
echo "Tag ${tag} exist."
exit 2
fi
echo "${0}: Git tag is checked OK: (${tag})"
fi

View File

@@ -0,0 +1,17 @@
#!/usr/bin/bash
## 提交新的 APK 编译配置标志信息并推送到Git仓库。
# 使用 `-z` 命令检查变量是否为空
if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ] || [ -z "$4" ]; then
echo "$0 Script parameter error."
echo "(Script Demo : [ bashCommitAppPublishBuildFlagInfo.sh <RootProjectDir> <VersionName> <BuildType Name> <RootProject Name> ])"
exit 2
fi
# 进入项目根目录
cd ${1}
echo -e "Work dir : \n"`pwd`
git add .
git commit -m "<$4>APK ${2} ${3} Publish."
git push origin && git push origin --tags

View File

@@ -0,0 +1,48 @@
#!/usr/bin/bash
## 提交新的 Library 编译配置标志信息并推送到Git仓库。
# 检查是否指定了将要发布的类库名称
# 使用 `-z` 命令检查变量是否为空
if [ -z "$1" ]; then
echo "Library name error: $0"
exit 2
fi
## 开始执行脚本
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 ..
fi
## 本脚本需要在项目根目录下执行
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 -e "Work dir error."
exit 1
fi
# 就读取脚本 .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="v7.6.4-test1"
# 正式设置标签时使用
#tag="v"${PUBLISH_VERSION}
git add .
git commit -m "<$1>Library Release ${PUBLISH_VERSION}"
git push origin && git push origin --tags

View File

@@ -0,0 +1,223 @@
#!/usr/bin/bash
# ==============================================================================
# WinBoLL 应用发布脚本
# 功能检查Git源码状态 → 编译Stage Release包 → 添加WinBoLL标签 → 提交并推送源码
# 依赖build.properties、app_update_description.txt项目根目录下
# 使用:./script_name.sh <APP_NAME>
# 作者:豆包&ZhanGSKen<zhangsken@qq.com>
# ==============================================================================
# ==================== 常量定义 ====================
# 脚本退出码
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
echo "[ERROR] 未指定应用名称!使用方式:${0} <APP_NAME>"
exit ${EXIT_CODE_ERR_NO_APP_NAME}
fi
APP_NAME=$1
echo "[INFO] 待发布应用名称:${APP_NAME}"
# 2. 检查并切换到项目根目录确保build.properties存在
echo "[INFO] 当前工作目录:$(pwd)"
if [[ ! -e "${APP_NAME}/build.properties" ]]; then
echo "[WARNING] 当前目录不存在${APP_NAME}/build.properties尝试切换到上级目录..."
cd ..
echo "[INFO] 切换后工作目录:$(pwd)"
fi
# 验证最终工作目录是否正确
if [[ ! -e "${APP_NAME}/build.properties" ]]; then
echo "[ERROR] 工作目录错误!${APP_NAME}/build.properties 文件不存在。"
exit ${EXIT_CODE_ERR_WORK_DIR}
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
echo "[ERROR] 源码与标签推送失败!"
exit 1
fi
# ==================== 主流程结束 ====================
echo "============================================="
echo " WinBoLL 应用发布完成!"
echo "============================================="
exit ${EXIT_CODE_SUCCESS}

View File

@@ -0,0 +1,166 @@
#!/usr/bin/bash
# 检查是否指定了将要发布的调试版应用名称
# 使用 `-z` 命令检查变量是否为空
if [ -z "$1" ]; then
echo "No APP name specified : $0"
exit 2
fi
## 定义相关函数
## 检查 Git 源码是否完全提交了完全提交就返回0
function checkGitSources {
#local input="$1"
#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="v7.6.4-test1"
# 正式调试版设置标签时使用
tag=$1"-v"${PUBLISH_VERSION}"-debug"
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="v7.6.4-beta"
# 正式设置标签时使用
tag=$1"-"v"${BASE_BETA_VERSION}-beta-debug
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 ..
fi
## 本脚本需要在项目根目录下执行
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 -e "Work dir error."
exit 1
fi
# 检查源码状态
result=$(checkGitSources)
if [[ $? -eq 0 ]]; then
echo $result
# 如果Git已经提交了所有代码就执行标签和应用发布操作
# 预先询问是否添加工作流标签
echo "Add Github Workflows Tag? (yes/no)"
result=$(askAddWorkflowsTag)
nAskAddWorkflowsTag=$?
echo $result
# 发布应用
echo "Publishing WinBoLL Debug APK ..."
# 脚本调试时使用
#bash gradlew :$1:assembleBetaDebug
# 正式发布调试版
bash gradlew :$1:assembleStageDebug
echo "Publishing WinBoLL Debug 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 Debug 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

@@ -0,0 +1,14 @@
#!/usr/bin/bash
# 检查是否指定了将要发布的类库名称
# 使用 `-z` 命令检查变量是否为空
if [ -z "$1" ]; then
echo "No Library name specified : $0"
exit 2
fi
## 正式发布使用
git pull && bash gradlew :$1:publishReleasePublicationToWinBoLLReleaseRepository && bash .winboll/bashCommitLibReleaseBuildFlagInfo.sh $1
## 调试使用
#bash gradlew :$1:publishSnapshotWinBoLLPublicationToWinBoLLSnapshotRepository && bash .winboll/bashCommitLibReleaseBuildFlagInfo.sh $1

View File

@@ -0,0 +1,342 @@
// WinBoLL 应用签名配置
//
android {
def winbollProps = new Properties()
def winbollPropsFile = rootProject.file("${winbollFilePath}")
if(winbollPropsFile.exists()) {
winbollProps.load(new FileInputStream(winbollPropsFile))
}
// 读取秘钥配置文件
//
def keyProps = new Properties()
def keyPropsFile = rootProject.file("${keyPropsFilePath}")
//println 'Test keystore path'
//println 'KeyProsFile :' + Boolean.toString(keyPropsFile.exists())
//assert(false)
if(keyPropsFile.exists()) {
keyProps.load(new FileInputStream(keyPropsFile))
}
// 配置签名
signingConfigs {
if(keyPropsFile.exists()) {
winboll {
assert(keyProps['keyAlias'] != null && keyProps['keyPassword'] != null && keyProps['storeFile'] != null && keyProps['storePassword'] != null)
keyAlias keyProps['keyAlias']
keyPassword keyProps['keyPassword']
storeFile keyProps['storeFile'] ? file(keyProps['storeFile']) : null
storePassword keyProps['storePassword']
}
}
}
buildTypes {
debug {
if(keyPropsFile.exists()) {
signingConfig signingConfigs.winboll
}
}
release {
if(keyPropsFile.exists()) {
signingConfig signingConfigs.winboll
}
minifyEnabled true // 开启混淆(核心开关)
shrinkResources true // 可选:移除无用资源(进一步减小体积)
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), // 官方默认规则(优化版)
'proguard-rules.pro' // 自定义规则文件
}
}
flavorDimensions "WinBoLLApp"
productFlavors {
beta {
// 检查编译标志位配置
assert (winbollBuildProps['buildCount'] != null)
dimension "WinBoLLApp"
applicationIdSuffix ".beta"
LocalDateTime localDateTimeNow = LocalDateTime.now(ZoneId.of("Asia/Shanghai"));
versionNameSuffix "-beta" + winbollBuildProps['buildCount'] + "_" + localDateTimeNow.format('mmss')
}
stage {
dimension "WinBoLLApp"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
// 应用包输出配置
//
android.applicationVariants.all { variant ->
//
// GitHub 应用包输出配置
// 1. 配置 Beta Release 版应用包输出
// 注意 GitHub 打包使用 android.yml 的 "bash ./gradlew assembleBetaRelease" 命令
//
if(variant.flavorName == "beta" && variant.buildType.name == "release") {
/* 后期管理预留代码 */
/* 暂时没有需要的 GitHub 应用包输出配置 */
/* GitHub 部分代码忽略 */
}
//
// WinBoLL 应用包输出配置
// 1. 配置 Stage Release 版应用包输出
// 2. 配置 Beta Debug 版应用包输出
//
if((variant.flavorName == "beta" && variant.buildType.name == "debug")
|| (variant.flavorName == "beta" && variant.buildType.name == "release")
|| (variant.flavorName == "stage" && variant.buildType.name == "debug")
|| (variant.flavorName == "stage" && variant.buildType.name == "release")) {
println "Project root directory: " + project.rootDir.toString()
println "Project root directory name : " + project.rootDir.name
def outputPath="${project.projectDir.absolutePath}/build/outputs/apk/${variant.buildType.name}"
//def outputFileName="${rootProject.name}_${versionName}.apk"
def outputFileName=project.rootDir.name + "_${versionName}.apk"
// 创建 WinBoLL Studio 发布接口文件夹
File fWinBoLLStudioDir = file("/sdcard/WinBoLLStudio/APKs");
// 如果配置了APK接口文件夹路径就设置应用APK输出文件夹为接口文件夹。
if(winbollProps != null && winbollProps['APKOutputPath'] != null ) {
fWinBoLLStudioDir = file(winbollProps['APKOutputPath']);
}
if(!fWinBoLLStudioDir.exists()) {
println "[ WinBoLLStudio ] : " + fWinBoLLStudioDir.getAbsolutePath() + " Folder does not exist."
println '[ WinBoLLStudio ] : The APKOutputPath property is not defined in winboll.properties, please configure APK output folder first.'
} else {
/// WINBOLL 主机的 APK 发布和源码管理操作 ///
variant.getAssembleProvider().get().doFirst {
/* 后期管理预留代码 */
} //doFirst {
// 编译输出后处理文件部分
//
variant.getAssembleProvider().get().doLast {
variant.outputs.forEach{ file->
// 如果正在调试,就拷贝到 WinBoLL 备份管理文件夹
//
if(variant.flavorName == "beta"&&variant.buildType.name == "debug"){
//File outBuildBckDir = new File(fWinBoLLStudioDir, "/${rootProject.name}/${variant.buildType.name}")
File outBuildBckDir = new File(fWinBoLLStudioDir, "/" + project.rootDir.name + "/${variant.buildType.name}")
// 创建目标路径目录
if(!outBuildBckDir.exists()) {
outBuildBckDir.mkdirs();
println "Output Folder Created.(WinBoLLStudio) : " + outBuildBckDir.getAbsolutePath()
}
if(outBuildBckDir.exists()) {
copy{
from file.outputFile
into outBuildBckDir
rename {
String fileName -> "${outputFileName}"
}
println "Output APK (WinBoLLStudio): " + outBuildBckDir.getAbsolutePath() + "/${outputFileName}"
}
// 检查编译标志位配置
assert (winbollBuildProps['buildCount'] != null)
assert (winbollBuildProps['libraryProject'] != null)
//构建计数增加
int buildCount = Integer.parseInt(winbollBuildProps['buildCount']) + 1;
// 设置编译计数
winbollBuildProps.setProperty("buildCount", Integer.toString(buildCount));
//保存编译标志配置
FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile)
winbollBuildProps.store(fos, "${winbollBuildPropsDesc}");
fos.close();
println "\n\n>>> Project build.properties saved.\n\n";
if(winbollBuildProps['libraryProject'] != "") {
// 如果应用 build.properties 文件设置了类库模块项目文件名
// 就拷贝一份新的编译标志配置到类库项目文件夹
File libraryProjectBuildPropsFile = new File("$RootProjectDir/" + winbollBuildProps['libraryProject'] + "/build.properties")
assert(winbollBuildPropsFile.exists())
assert(libraryProjectBuildPropsFile.exists())
java.nio.file.Path sourceFilePath = winbollBuildPropsFile.toPath();
java.nio.file.Path targetFilePath = libraryProjectBuildPropsFile.toPath();
// 使用copyTo()方法复制文件,如果目标文件存在会被覆盖,可选参数可以选择不覆盖
java.nio.file.Files.copy(sourceFilePath, targetFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
println "\n\n>>> Library Project build.properties saved.\n\n";
}
}
}
// 如果正在发布,就拷贝到 WinBoLL 标签管理文件夹,和处理 Git 仓库管理任务。
//
if(variant.flavorName == "stage"&&variant.buildType.name == "release"){
// 发布 APK 文件
//
// 截取版本号的版本字段为短版本名
String szVersionName = "${versionName}"
String[] szlistTemp = szVersionName.split("-")
String szShortVersionName = szlistTemp[0]
//String szCommonTagAPKName = "${rootProject.name}_" + szShortVersionName + ".apk"
String szCommonTagAPKName = project.rootDir.name + "_" + szShortVersionName + ".apk"
println "CommonTagAPKName is : " + szCommonTagAPKName
//File outTagDir = new File(fWinBoLLStudioDir, "/${rootProject.name}/tag/")
File outTagDir = new File(fWinBoLLStudioDir, "/" + project.rootDir.name + "/tag/")
// 创建目标路径目录
if(!outTagDir.exists()) {
outTagDir.mkdirs();
println "Output Folder Created.(Tags) : " + outTagDir.getAbsolutePath()
}
if(outTagDir.exists()) {
File targetAPK = new File(outTagDir, "${szCommonTagAPKName}")
if(targetAPK.exists()) {
// 标签版本APK文件已经存在构建拷贝任务停止
assert (!targetAPK.exists())
// 可选择删除并继续输出APK文件
//delete targetAPK
}
// 复制一个备份
copy{
from file.outputFile
into outTagDir
rename {
String fileName -> "${outputFileName}"
}
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${outputFileName}"
}
// 复制一个并重命名为短版本名
copy{
from file.outputFile
into outTagDir
rename {
String fileName -> "${szCommonTagAPKName}"
}
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${szCommonTagAPKName}"
}
// 检查编译标志位配置
assert (winbollBuildProps['stageCount'] != null)
assert (winbollBuildProps['publishVersion'] != null)
assert (winbollBuildProps['buildCount'] != null)
assert (winbollBuildProps['baseVersion'] != null)
assert (winbollBuildProps['baseBetaVersion'] != null)
assert (winbollBuildProps['libraryProject'] != null)
// 设置类库的默认版本名
winbollBuildProps.setProperty("publishVersion", "${versionName}");
// Stage 发布计数增加
int stageCount = Integer.parseInt(winbollBuildProps['stageCount']) + 1;
winbollBuildProps.setProperty("stageCount", Integer.toString(stageCount));
// 设置类库的默认Beta版本名
winbollBuildProps.setProperty("baseBetaVersion", winbollBuildProps['baseVersion'] + "." + Integer.toString(stageCount));
// 构建计数重置
winbollBuildProps.setProperty("buildCount", "0");
//保存编译标志配置
FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile)
winbollBuildProps.store(fos, "${winbollBuildPropsDesc}");
fos.close();
if(winbollBuildProps['libraryProject'] != "") {
// 如果应用 build.properties 文件设置了类库模块项目文件名
// 就拷贝一份新的编译标志配置到类库项目文件夹
File libraryProjectBuildPropsFile = new File("$RootProjectDir/" + winbollBuildProps['libraryProject'] + "/build.properties")
assert(winbollBuildPropsFile.exists())
assert(libraryProjectBuildPropsFile.exists())
java.nio.file.Path sourceFilePath = winbollBuildPropsFile.toPath();
java.nio.file.Path targetFilePath = libraryProjectBuildPropsFile.toPath();
// 使用copyTo()方法复制文件,如果目标文件存在会被覆盖,可选参数可以选择不覆盖
java.nio.file.Files.copy(sourceFilePath, targetFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
// 提交新的编译标志配置
println 'exec bashCommitAppPublishBuildFlagInfoFilePath ...'
def resultCommitBuildFlag = exec {
commandLine 'bash', '--', "${RootProjectDir}/${bashCommitAppPublishBuildFlagInfoFilePath}", "${RootProjectDir}", "${versionName}", variant.buildType.name , rootProject.name
}
// 检查bash命令的返回值假设非零表示失败
assert(resultCommitBuildFlag.getExitValue() == 0)
}
} // if(variant.buildType.name == "release"){
// 如果正在调试发布版就只生成和输出APK文件不处理 Git 仓库提交与更新问题。
//
if(variant.flavorName == "stage"&&variant.buildType.name == "debug"){
// 发布 APK 文件
//
// 截取版本号的版本字段为短版本名
String szVersionName = "${versionName}"
String[] szlistTemp = szVersionName.split("-")
String szShortVersionName = szlistTemp[0]
//String szCommonTagAPKName = "${rootProject.name}_" + szShortVersionName + ".apk"
String szCommonTagAPKName = project.rootDir.name + "_" + szShortVersionName + ".apk"
println "CommonTagAPKName is : " + szCommonTagAPKName
//File outTagDir = new File(fWinBoLLStudioDir, "/${rootProject.name}/tag/")
File outTagDir = new File(fWinBoLLStudioDir, "/" + project.rootDir.name + "/${variant.buildType.name}/")
// 创建目标路径目录
if(!outTagDir.exists()) {
outTagDir.mkdirs();
println "Output Folder Created.(Tags) : " + outTagDir.getAbsolutePath()
}
if(outTagDir.exists()) {
File targetAPK = new File(outTagDir, "${szCommonTagAPKName}")
if(targetAPK.exists()) {
// 标签版本APK文件已经存在构建拷贝任务停止
println '如果是在调试 Stage 版应用包构建,请删除(注在debug目录)现有的 Stage 应用包('+targetAPK.getAbsolutePath()+')。再编译一次。'
assert (!targetAPK.exists())
// 可选择删除并继续输出APK文件
//delete targetAPK
}
// 复制一个备份
copy{
from file.outputFile
into outTagDir
rename {
String fileName -> "${outputFileName}"
}
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${outputFileName}"
}
// 复制一个并重命名为短版本名
copy{
from file.outputFile
into outTagDir
rename {
String fileName -> "${szCommonTagAPKName}"
}
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${szCommonTagAPKName}"
}
//不保存编译标志配置
}
}
// 如果配置了APK额外输出路径就复制一份拷贝到额外路径。
//
if(winbollProps != null && winbollProps['ExtraAPKOutputPath'] != null ) {
File apkFile = new File(winbollProps['ExtraAPKOutputPath'])
File outCommonDir = apkFile.getParentFile();
String commandAPKName = apkFile.getName();
if(outCommonDir.exists()) {
copy{
from file.outputFile
into outCommonDir
rename {
String fileName -> "${commandAPKName}"
}
println "Output APK (Common): " + outCommonDir.getAbsolutePath() + "/${commandAPKName}"
}
}
}
}
}// End of (variant.getAssembleProvider().get().doLast {)
}/// WINBOLL 主机的 APK 发布和源码管理操作结束 ///
}
} // End of (android.applicationVariants.all { variant ->)
}

View File

@@ -0,0 +1,211 @@
// 本机和 WinBoLL Maven 仓库传输配置。
//
def getDefaultVersion(){
// 检查编译标志位配置
assert (winbollBuildProps['publishVersion'] != null)
// 返回编译版本号
return winbollBuildProps['publishVersion']
}
def siteUrl = 'https://winboll.cc/?page=studio/details.php&app=${rootProject.name}' // 项目主页
def gitUrl = 'https://gitea.winboll.cc/WinBoLL/${rootProject.name}' // 项目的git地址
def DefaultGroupId = 'cc.winboll.studio' // 类库所有者groupId
def DefaultVersion = getDefaultVersion() // 版本号
def DeveloperId='zhangsken' // 开发者账号
def DeveloperName='ZhanGSKen' // 开发者名称
def DeveloperEMail='zhangsken@188.com' // 开发者邮箱地址
def LicenseName='The Apache Software License, Version 2.0'
def LicenseUrl='http://www.apache.org/licenses/LICENSE-2.0.txt'
Properties properties = new Properties()
afterEvaluate {
publishing {
repositories {
if(file("${RootProjectDir}/${winbollFilePath}").exists()) {
properties.load(file("${RootProjectDir}/${winbollFilePath}").newDataInputStream())
def NexusUserName = properties.getProperty("Nexus.name")
def NexusPassword = properties.getProperty("Nexus.password")
// WinBoLL Release 仓库
maven{
//仓库的名字和地址
name = "WinBoLLRelease"
url="https://nexus.winboll.cc/repository/maven-releases/"
// 仓库用户名密码
credentials {
username = NexusUserName
password = NexusPassword
}
}
// WinBoLL Snapshot 仓库
maven{
//仓库的名字和地址
name = "WinBoLLSnapshot"
url="https://nexus.winboll.cc/repository/maven-snapshots/"
// 仓库用户名密码
credentials {
username = NexusUserName
password = NexusPassword
}
}
}
}
publications {
// Local Maven 仓库传输任务
//
release(MavenPublication) {
groupId = DefaultGroupId
artifactId = project.name
version = DefaultVersion
//from components.java
// 必须有这个 否则不会上传AAR包
afterEvaluate { artifact(tasks.getByName("bundleReleaseAar")) }
// 上传source这样使用方可以看到方法注释
//artifact generateSourcesJar
//要上传的aar路径
//artifact "$buildDir/outputs/aar/${project.getName()}-release.aar"
//artifact "$buildDir/outputs/aar/${project.getName()}-debug.aar"
//对pom进行的操作
pom.withXml{
Node pomNode = asNode()
pomNode.dependencies.'*'.findAll() {
//将所有的默认依赖移除
//it.parent().remove(it)
}
}
pom {
name = artifactId
url = siteUrl
licenses {
license { //证书说明
name=LicenseName // 开源协议名称
url=LicenseUrl // 协议地址
}
}
developers {
developer {
id=DeveloperId // 开发者账号
name=DeveloperName // 开发者名称
email=DeveloperEMail // 开发者邮箱地址
}
}
//软件配置管理
scm {
connection=gitUrl
developerConnection=gitUrl
url=siteUrl
}
}
}
// WinBoLL Maven Release 仓库传输任务
//
releaseWinBoLL(MavenPublication) {
// 需要使用的变体假设有free和pay两个变体可以选择一个
//from components.free
groupId = DefaultGroupId // 文件的groupId
artifactId = project.name //文件的名字
version = DefaultVersion //版本号
//from components.java
// 必须有这个 否则不会上传AAR包
afterEvaluate { artifact(tasks.getByName("bundleReleaseAar")) }
// 上传source这样使用方可以看到方法注释
//artifact generateSourcesJar
//要上传的aar路径
//artifact "$buildDir/outputs/aar/${project.getName()}-release.aar"
//artifact "$buildDir/outputs/aar/${project.getName()}-debug.aar"
//对pom进行的操作
pom.withXml{
Node pomNode = asNode()
pomNode.dependencies.'*'.findAll() {
//将所有的默认依赖移除
//it.parent().remove(it)
}
}
pom {
name = artifactId
url = siteUrl
licenses {
license { //证书说明
name=LicenseName // 开源协议名称
url=LicenseUrl // 协议地址
}
}
developers {
developer {
id=DeveloperId // 开发者账号
name=DeveloperName // 开发者名称
email=DeveloperEMail // 开发者邮箱地址
}
}
//软件配置管理
scm {
connection=gitUrl
developerConnection=gitUrl
url=siteUrl
}
}
} // 创建名为 release 的任务结束
// WinBoLL Maven Snapshot 仓库传输任务
//
snapshotWinBoLL(MavenPublication) {
// 需要使用的变体假设有free和pay两个变体可以选择一个
//from components.free
groupId = DefaultGroupId // 文件的groupId
artifactId = project.name //文件的名字
version = DefaultVersion + "-SNAPSHOT" //版本号
//from components.java
// 必须有这个 否则不会上传AAR包
afterEvaluate { artifact(tasks.getByName("bundleReleaseAar")) }
// 上传source这样使用方可以看到方法注释
//artifact generateSourcesJar
//要上传的aar路径
//artifact "$buildDir/outputs/aar/${project.getName()}-release.aar"
//artifact "$buildDir/outputs/aar/${project.getName()}-debug.aar"
//对pom进行的操作
pom.withXml{
Node pomNode = asNode()
pomNode.dependencies.'*'.findAll() {
//将所有的默认依赖移除
//it.parent().remove(it)
}
}
pom {
name = artifactId
url = siteUrl
licenses {
license { //证书说明
name=LicenseName // 开源协议名称
url=LicenseUrl // 协议地址
}
}
developers {
developer {
id=DeveloperId // 开发者账号
name=DeveloperName // 开发者名称
email=DeveloperEMail // 开发者邮箱地址
}
}
//软件配置管理
scm {
connection=gitUrl
developerConnection=gitUrl
url=siteUrl
}
}
} // 创建名为 snapshot 的任务结束
}
}
}

View File

@@ -0,0 +1,50 @@
android {
lintOptions {
// true--关闭lint报告的分析进度
//quiet true
// true--错误发生后停止gradle构建
abortOnError false
// true--只报告error
//ignoreWarnings true
// true--忽略有错误的文件的全/绝对路径(默认是true)
//absolutePaths true
// true--检查所有问题点,包含其他默认关闭项
checkAllWarnings true
// true--所有warning当做error
//warningsAsErrors true
// 关闭指定问题检查
//disable 'ExpiredTargetSdkVersion','HardcodedText','UnknownNullness','ButtonStyle','GradleDependency','UnusedResources'
// 打开指定问题检查
//enable 'RtlHardcoded','RtlCompat', 'RtlEnabled'
// 仅检查指定问题
//check 'NewApi', 'InlinedApi'
// true--error输出文件不包含源码行号
//noLines true
// true--显示错误的所有发生位置,不截取
showAll true
// 回退lint设置(默认规则)
//lintConfig file("default-lint.xml")
// true--生成txt格式报告(默认false)
//textReport true
// 重定向输出;可以是文件或'stdout'
//textOutput 'stdout'
// true--生成XML格式报告
xmlReport true
// 指定xml报告文档(默认lint-results.xml)
xmlOutput file("${lintXmlReportFilePath}")
// true--生成HTML报告(带问题解释,源码位置,等)
htmlReport true
// html报告可选路径(构建器默认是lint-results.html )
htmlOutput file("${lintHTMLReportFilePath}")
// true--所有正式版构建执行规则生成崩溃的lint检查如果有崩溃问题将停止构建
checkReleaseBuilds true
// 在发布版本编译时检查(即使不包含lint目标),指定问题的规则生成崩溃
//fatal 'NewApi', 'InlineApi'
// 指定问题的规则生成错误
//error 'Wakelock', 'TextViewEdits'
// 指定问题的规则生成警告
//warning 'ResourceAsColor'
// 忽略指定问题的规则(同关闭检查)
//ignore 'TypographyQuotes'
}
}

View File

@@ -0,0 +1,49 @@
#!/bin/bash
# 应用秘钥创建脚本
# Linux 命令行创建JKS秘钥alias和keyAlias可配置文件名含时间戳
# 可配置参数(按需修改)
ALIAS="WinBoLL.CC_Debug" # 别名与keyAlias一致
STORE_PASS="androiddebugkey"
KEY_PASS="androiddebugkey"
COUNTRY="CN" # 国家代码
# 获取当前时间戳
TIMESTAMP=$(date +%Y%m%d%H%M%S)
FILENAME="${ALIAS}_${TIMESTAMP}.jks"
STORENAME="${ALIAS}_${TIMESTAMP}.keystore"
# 生成JKS文件alias与keyAlias同步
keytool -genkeypair \
-alias "${ALIAS}" \
-keyalg RSA \
-keysize 2048 \
-validity 1 \
-keystore "${FILENAME}" \
-dname "CN=WBFans, OU=Studio, O=WinBoLL, L=Shanwei, ST=Guangdong, C=${COUNTRY}" \
-storepass "${STORE_PASS}" \
-keypass "${KEY_PASS}"
# 写入配置文件
cat <<EOF > ${STORENAME}
keyAlias=${ALIAS}
keyPassword=${KEY_PASS}
storeFile=../appkey.jks
storePassword=${STORE_PASS}
EOF
echo "已生成秘钥:${FILENAME}"
echo "配置已写入 ${STORENAME}keyAlias=${ALIAS}"
# 询问是否复制文件
read -p "是否需要将文件复制为 appkey.jks 和 appkey.keystore(y/n): " CONFIRM
if [[ $CONFIRM =~ ^[Yy]$ ]]; then
# 复制 jks 文件为 appkey.jks
cp -v ${FILENAME} ../appkey.jks
# 复制 keystore 文件为 appkey.keystore
cp -v ${STORENAME} ../appkey.keystore
echo "文件复制完成"
else
echo "已取消文件复制"
fi

201
LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

97
LICENSE-Private-Demo Normal file
View File

@@ -0,0 +1,97 @@
# WinBoLL 源码 LICENSE-Private-Demo 规范说明书
# LICENSE-Private-Demo
# WinBoLL 源码公共转私有继承开发规范守则
## 核心声明
本文档**唯一核心设计目的**:通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
## 一、文件宗旨与风险防控说明
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
1. 严格隔离公共开源分支与私有开发分支,通过授权文件标记实现分支属性一眼可辨,杜绝人为操作混淆
2. **重点防控人为操作导致的私有分支代码违规合并、回合、推送至公共 ****`winboll`**** 主流分支**,从流程上封堵合并风险
3. 明确所有开发提交者的操作责任,违规合并公共分支的行为由操作人承担全部代码泄露、合规风险
4. 规范私有分支初始化全流程,保证私有分支仅可单向同步公共分支更新,禁止任何反向代码流入公共分支
## 二、公私分支授权标识文件定义(风控核心依据)
### 1. 公共开源分支唯一标识
**文件名LICENSE**
- 仅允许存在于公共主流分支 `winboll` 及官方公共衍生分支
- 标识当前分支为**开源公开可贡献分支**,遵循原开源授权协议
- **严禁私有分支内保留、恢复此文件**,出现即判定分支属性异常
### 2. 私有开发分支唯一标识
**文件名LICENSE-Private**
- 仅允许存在于私有开发分支,**绝对禁止出现在公共 ****`winboll`**** 分支**
- 标识当前分支为**私有闭源分支**,代码仅限内部使用,禁止公开、禁止对外贡献
- 为本分支私有属性的法定判定依据,也是禁止合并至公共分支的核心标记
## 三、分支管理与合并风控规则(强制遵守)
1. **公共主流分支**:固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
2. **私有开发分支**:统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
3. **核心合并风控铁则**
- 私有分支 → 公共分支:**永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
- 公共分支 → 私有分支:允许正常拉取、同步上游更新,不影响私有开发迭代
4. 所有仓库提交者、合并操作者,均视为已阅读并完全认可本规则,**人为执行私有分支向公共分支的合并操作,由操作人承担全部代码泄露、合规违约、项目安全风险**。
## 四、公共转私有标准化操作步骤(锁死合并风险)
请严格按顺序执行,每一步均为风控必要环节,不可跳过、不可修改顺序。
1. 基于公共主流分支 `winboll`,新建私有开发分支,严格使用 `private-demo-*` 命名,从名称上明确分支私有属性,避免人为混淆。
2. 本地仓库切换至新建私有分支,确认当前分支名称、检出来源无误。
3. **永久删除项目根目录公共授权文件 ****`LICENSE`**,彻底移除公共分支标识,断绝误合并的标识漏洞。
4. 将本规范文件 `LICENSE-Private-Demo` 复制并重命名为 `LICENSE-Private`,作为私有分支生效授权文件。
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**
> 初始化私有开发分支,已切换私有授权文件,本分支禁止任何人为合并、推送至 winboll 公共分支
>
>
6. 提交完成后,本分支正式转为私有开发状态,后续所有代码提交、分支合并、版本迭代,均严禁指向公共 `winboll` 分支。
## 五、人为操作责任界定(核心补充条款)
1. 本分支所有开发者、代码提交者、分支合并操作者,均视为**完全知晓本分支的私有属性与合并禁令**,自愿遵守本规范全部约束。
2. **无论故意或过失,凡是人为执行私有分支向公共 ****`winboll`**** 分支的合并、推送、PR 提交、代码回合操作,全部责任由执行操作的本人独立承担**,项目方不承担任何因人为违规操作导致的代码泄露、开源合规、版本污染风险。
3. 仓库管理员需严格校验合并请求的分支标识与授权文件,发现带有 `LICENSE-Private` 标记的分支申请合并至公共分支,一律直接拒绝,并记录操作人信息。
4. 分支属性校验以根目录授权文件为唯一标准:只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
## 六、分支状态校验与异常处理
- 合规公共分支:仅存在 `LICENSE`,无 `LICENSE-Private`
- 合规私有分支:仅存在 `LICENSE-Private`,无 `LICENSE`
- 异常状态:两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
> (注:文档部分内容可能由 AI 生成)

View File

@@ -0,0 +1,97 @@
# WinBoLL 源码 LICENSE-Private-Demo 规范说明书
# LICENSE-Private-Demo
# WinBoLL 源码公共转私有继承开发规范守则
## 核心声明
本文档**唯一核心设计目的**:通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
## 一、文件宗旨与风险防控说明
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
1. 严格隔离公共开源分支与私有开发分支,通过授权文件标记实现分支属性一眼可辨,杜绝人为操作混淆
2. **重点防控人为操作导致的私有分支代码违规合并、回合、推送至公共 ****`winboll`**** 主流分支**,从流程上封堵合并风险
3. 明确所有开发提交者的操作责任,违规合并公共分支的行为由操作人承担全部代码泄露、合规风险
4. 规范私有分支初始化全流程,保证私有分支仅可单向同步公共分支更新,禁止任何反向代码流入公共分支
## 二、公私分支授权标识文件定义(风控核心依据)
### 1. 公共开源分支唯一标识
**文件名LICENSE**
- 仅允许存在于公共主流分支 `winboll` 及官方公共衍生分支
- 标识当前分支为**开源公开可贡献分支**,遵循原开源授权协议
- **严禁私有分支内保留、恢复此文件**,出现即判定分支属性异常
### 2. 私有开发分支唯一标识
**文件名LICENSE-Private**
- 仅允许存在于私有开发分支,**绝对禁止出现在公共 ****`winboll`**** 分支**
- 标识当前分支为**私有闭源分支**,代码仅限内部使用,禁止公开、禁止对外贡献
- 为本分支私有属性的法定判定依据,也是禁止合并至公共分支的核心标记
## 三、分支管理与合并风控规则(强制遵守)
1. **公共主流分支**:固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
2. **私有开发分支**:统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
3. **核心合并风控铁则**
- 私有分支 → 公共分支:**永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
- 公共分支 → 私有分支:允许正常拉取、同步上游更新,不影响私有开发迭代
4. 所有仓库提交者、合并操作者,均视为已阅读并完全认可本规则,**人为执行私有分支向公共分支的合并操作,由操作人承担全部代码泄露、合规违约、项目安全风险**。
## 四、公共转私有标准化操作步骤(锁死合并风险)
请严格按顺序执行,每一步均为风控必要环节,不可跳过、不可修改顺序。
1. 基于公共主流分支 `winboll`,新建私有开发分支,严格使用 `private-demo-*` 命名,从名称上明确分支私有属性,避免人为混淆。
2. 本地仓库切换至新建私有分支,确认当前分支名称、检出来源无误。
3. **永久删除项目根目录公共授权文件 ****`LICENSE`**,彻底移除公共分支标识,断绝误合并的标识漏洞。
4. 将本规范文件 `LICENSE-Private-Demo` 复制并重命名为 `LICENSE-Private`,作为私有分支生效授权文件。
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**
> 初始化私有开发分支,已切换私有授权文件,本分支禁止任何人为合并、推送至 winboll 公共分支
>
>
6. 提交完成后,本分支正式转为私有开发状态,后续所有代码提交、分支合并、版本迭代,均严禁指向公共 `winboll` 分支。
## 五、人为操作责任界定(核心补充条款)
1. 本分支所有开发者、代码提交者、分支合并操作者,均视为**完全知晓本分支的私有属性与合并禁令**,自愿遵守本规范全部约束。
2. **无论故意或过失,凡是人为执行私有分支向公共 ****`winboll`**** 分支的合并、推送、PR 提交、代码回合操作,全部责任由执行操作的本人独立承担**,项目方不承担任何因人为违规操作导致的代码泄露、开源合规、版本污染风险。
3. 仓库管理员需严格校验合并请求的分支标识与授权文件,发现带有 `LICENSE-Private` 标记的分支申请合并至公共分支,一律直接拒绝,并记录操作人信息。
4. 分支属性校验以根目录授权文件为唯一标准:只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
## 六、分支状态校验与异常处理
- 合规公共分支:仅存在 `LICENSE`,无 `LICENSE-Private`
- 合规私有分支:仅存在 `LICENSE-Private`,无 `LICENSE`
- 异常状态:两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
> (注:文档部分内容可能由 AI 生成)

View File

@@ -0,0 +1,97 @@
# WinBoLL 源码 LICENSE\-Private\-Demo 规范说明书
# LICENSE\-Private\-Demo
# WinBoLL 源码公共转私有继承开发规范守则
## 核心声明
本文档**唯一核心设计目的**:通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
## 一、文件宗旨与风险防控说明
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
1. 严格隔离公共开源分支与私有开发分支,通过授权文件标记实现分支属性一眼可辨,杜绝人为操作混淆
2. **重点防控人为操作导致的私有分支代码违规合并、回合、推送至公共 ****`winboll`**** 主流分支**,从流程上封堵合并风险
3. 明确所有开发提交者的操作责任,违规合并公共分支的行为由操作人承担全部代码泄露、合规风险
4. 规范私有分支初始化全流程,保证私有分支仅可单向同步公共分支更新,禁止任何反向代码流入公共分支
## 二、公私分支授权标识文件定义(风控核心依据)
### 1\. 公共开源分支唯一标识
**文件名LICENSE**
- 仅允许存在于公共主流分支 `winboll` 及官方公共衍生分支
- 标识当前分支为**开源公开可贡献分支**,遵循原开源授权协议
- **严禁私有分支内保留、恢复此文件**,出现即判定分支属性异常
### 2\. 私有开发分支唯一标识
**文件名LICENSE\-Private**
- 仅允许存在于私有开发分支,**绝对禁止出现在公共 ****`winboll`**** 分支**
- 标识当前分支为**私有闭源分支**,代码仅限内部使用,禁止公开、禁止对外贡献
- 为本分支私有属性的法定判定依据,也是禁止合并至公共分支的核心标记
## 三、分支管理与合并风控规则(强制遵守)
1. **公共主流分支**:固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
2. **私有开发分支**:统一从 `winboll` 分支检出,命名固定格式为 `private\-demo\-\*`,与公共分支物理隔离。
3. **核心合并风控铁则**
- 私有分支 → 公共分支:**永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
- 公共分支 → 私有分支:允许正常拉取、同步上游更新,不影响私有开发迭代
4. 所有仓库提交者、合并操作者,均视为已阅读并完全认可本规则,**人为执行私有分支向公共分支的合并操作,由操作人承担全部代码泄露、合规违约、项目安全风险**。
## 四、公共转私有标准化操作步骤(锁死合并风险)
请严格按顺序执行,每一步均为风控必要环节,不可跳过、不可修改顺序。
1. 基于公共主流分支 `winboll`,新建私有开发分支,严格使用 `private\-demo\-\*` 命名,从名称上明确分支私有属性,避免人为混淆。
2. 本地仓库切换至新建私有分支,确认当前分支名称、检出来源无误。
3. **永久删除项目根目录公共授权文件 ****`LICENSE`**,彻底移除公共分支标识,断绝误合并的标识漏洞。
4. 将本规范文件 `LICENSE\-Private\-Demo` 复制并重命名为 `LICENSE\-Private`,作为私有分支生效授权文件。
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**
> 初始化私有开发分支,已切换私有授权文件,本分支禁止任何人为合并、推送至 winboll 公共分支
>
>
6. 提交完成后,本分支正式转为私有开发状态,后续所有代码提交、分支合并、版本迭代,均严禁指向公共 `winboll` 分支。
## 五、人为操作责任界定(核心补充条款)
1. 本分支所有开发者、代码提交者、分支合并操作者,均视为**完全知晓本分支的私有属性与合并禁令**,自愿遵守本规范全部约束。
2. **无论故意或过失,凡是人为执行私有分支向公共 ****`winboll`**** 分支的合并、推送、PR 提交、代码回合操作,全部责任由执行操作的本人独立承担**,项目方不承担任何因人为违规操作导致的代码泄露、开源合规、版本污染风险。
3. 仓库管理员需严格校验合并请求的分支标识与授权文件,发现带有 `LICENSE\-Private` 标记的分支申请合并至公共分支,一律直接拒绝,并记录操作人信息。
4. 分支属性校验以根目录授权文件为唯一标准:只要分支内存在 `LICENSE\-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
## 六、分支状态校验与异常处理
- 合规公共分支:仅存在 `LICENSE`,无 `LICENSE\-Private`
- 合规私有分支:仅存在 `LICENSE\-Private`,无 `LICENSE`
- 异常状态:两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
> (注:文档部分内容可能由 AI 生成)

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 KiB

105
README.md Normal file
View File

@@ -0,0 +1,105 @@
# WinBoLL 源生态计划项目说明书
## 一、项目概述
### 1. 核心定位
WinBoLL 手机源码计划,旨在通过核心项目 WinBoLL 构建手机端与服务器端的 Android 项目的开发源码生态。实现手机与服务器的源码的联合开发。
### 2. 仓库架构
#### **仓库类型:功能说明**
☆ 基础项目分支 WinBoLL手机端安卓应用开发基础模板。
☆ 应用项目分支 APPBase、AES、PowerBell、Positions**:安卓应用单一管理系列项目。
☆ 源码汇总管理 OriginMaster**:各类分支源码合并存档,不适宜作为开发库使用。
### 3. 源码合并管理推送路线图
⚠️ **注意**:仅仅展示不同应用模块源码的综合管理路线。分支合并操作时,必须具备 Git 管理经验。
★ WinBoLL → APPBase → OriginMaster
★ WinBoLL → AES → OriginMaster
★ WinBoLL → PowerBell → OriginMaster
★ WinBoLL → Positions → OriginMaster
## 二、WinBoLL 项目核心信息
### 1. 项目简介
☆ WinBoLL 项目是为手机端开发Android 项目的需求而设计的项目。
### 2. 官方资源
#### ☆ 官方网站**https://www.winboll.cc/
#### ☆ 源码地址:
★ Giteahttps://gitea.winboll.cc/Studio/WinBoLL.git
★ GitHubhttps://github.com/ZhanGSKen/WinBoLL.git
★ 码云https://gitee.com/zhangsken/winboll.git
## 三、应用编译环境检查问题
### 核心判断条件:
☆ WinBoLL 项目以文件夹 `"/sdcard/WinBoLLStudio/APKs"` 是否存在为判断环境编译输出条件因为编译输出的APK文件需要一个可供保存的环境。
☆ 文件夹"/sdcard/WinBoLLStudio/APKs" 目录条件设置方法:
***Linux 服务器端方面***:建立 `/sdcard/WinBoLLStudio/APKs` 目录即可。
***手机开发端方面***:建立 `"/sdcard/WinBoLLStudio/APKs"` 目录(即 `"/storage/emulated/0/WinBoLLStudio/APKs"` 目录) 即可。
## 四、前置条件
### 1. WinBoLL APP 开发环境配置介绍
#### WinBoLL APK 编译输出内容包括:
☆ "/sdcard/WinBoLLStudio/APKs"` 目录内的所有应用分支的 APK 文件。
winboll.properties 文件的 APKOutputPath 属性可配置这个 APK 输出目录的路径。
☆ "/sdcard/AppProjects/app.apk"文件。
winboll.properties 文件的 ExtraAPKOutputPath 属性可配置这个 APK 额外输出文件的路径
#### WinBoLL APK 源码命名空间规范
☆ WinBoLL 项目使用 "cc.winboll.studio" 作为源码命名空间。在此命名空间下进行源码定义。
## 五、核心需求规划
### 1. WinBoLL 应用安全验证需求
#### ☆ 支持访问 https://console.winboll.cc/ 服务器以校验应用包签名与版本。
### 2. 手机端源码开发管理需求
#### ☆ 支持切换不同 WinBoLL 分支,以开发不同安卓应用。
## 六、编译与使用指南
### 1. 项目初始化(必须)
#### 1. 复制 `settings.gradle-demo` 为 `settings.gradle`。编辑 `settings.gradle` 文件内容,取消对应项目模块注释。
#### 2. 复制 `gradle.properties-androidx-demo` (Android X 项目) 或 `gradle.properties-android-demo` (基本 Android 项目) 为 `gradle.properties`。
#### 3. 复制(可选)`local.properties-demo` 为 `local.properties`,编辑 `local.properties` 文件内容,配置 Android SDK 目录。
#### 4. **签名设置**
☆ **调试编译秘钥制作**:使用 Termux 应用终端cd 进入 GenKeyStore 目录,运行 `bash gen_debug_keystore.sh` 脚本即可生成应用调试秘钥。
☆ **应用秘钥配置方法**:拷贝调试编译秘钥制作生成的 `appkey.jks` 与 `appkey.keystore` 文件到项目根目录即可。
## 七、应用编译命令介绍
### 1类库型模块配置要点
#### 1. **优先修改配置文件**:优先修改应用测试项目(目录为 `"<WinBoLl根目录>/<类库测试应用>/"`)内 `build.properties` 文件,设置对应的类库项目名称:`libraryProject=<类库项目模块名>`。
#### 2. **编译优先启动步骤**:使用 Termux 应用,进入 `"<WinBoLl根目录>"`,运行 `$ bash .winboll/bashPublishAPKAddTag.sh <类库测试项目模块名>` 命令。运行后可生成测试项目与类库项目的编译参数文件 `build.properties`。生成的 `build.properties` 文件有两份,一份在测试项目模块的文件夹内,一份在类库项目本身的模块文件夹内。
#### 3. **最后类库编译发布步骤**:使用 Termux 应用,进入 `"<WinBoLl根目录>"`,运行 `$ bash .winboll/bashPublishLIBAddTag.sh <类库项目模块名>` 命令。运行后可发布至 WinBoLL Nexus Maven 库、本地 maven 目录或者是通用默认的 Gradle Maven 库。
### 2单一应用型模块与类库测试型模块配置要点
#### ☆ APK 编译方法:
使用 Termux 应用,进入 `"<WinBoLl根目录>"`,运行 `$ bash .winboll/bashPublishAPKAddTag.sh <应用项目模块名>`。
#### ☆ 运行后的 APK 输出路径:
★ 默认路径 (`$ bash gradlew assembleBetaDebug` 任务)APK 在 `/sdcard/WinBoLLStudio/APKs/<项目根目录名称>/debug/` 目录。
★ 默认路径 (`$ bash assembleStageRelease` 任务)APK 在 `/sdcard/WinBoLLStudio/APKs/<项目根目录名称>/tag/` 目录。
★ 额外输出路径:(假设 `winboll.properties` 文件已配置 `ExtraAPKOutputPath` 属性) 输出至 `ExtraAPKOutputPath` 属性配置的目录下。
### 3手机端应用调试命令介绍
#### ☆ Beta 渠道调试命令
$bash gradlew assembleBetaDebug
#### ☆ Stage 渠道调试命令
$bash gradlew assembleStageDebug
### 4服务器端开发命令介绍
##### ☆ Stage 渠道应用发布命令为:
"<WinBoLl根目录>/settings.gradle"文件需要配置编译模块开启参数,拷贝 settings.gradle-demo 为 settings.gradle 文件取消对应的分支配置部分即可。)
$bash .winboll/bashPublishAPKAddTag.sh <应用项目模块名> 
或者是
$bash gradlew assembleStageRelease
## 八、WinBoLL 应用 APK 版本号命名规则
### ☆ Stage 渠道:
#### V<应用开发环境编号><应用功能变更号><应用调试阶段号> 示例 APPBase_15.7.0 
### ☆ Beta 渠道:
#### V<应用开发环境编号><应用功能变更号><应用调试阶段号>-beta<调试编译计数>_<调试编译时间(分钟+秒钟)> 示例 APPBase_15.9.6-beta8_5413 

1
aes/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

36
aes/README.md Normal file
View File

@@ -0,0 +1,36 @@
# AES
[![](https://jitpack.io/v/ZhanGSKen/AES.svg)](https://jitpack.io/#ZhanGSKen/AES)
#### 介绍
WinBoLL AndroidX 可视化元素类库。
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
#### Gradle 编译说明
调试版编译命令 gradle assembleBetaDebug
阶段版编译命令 bash .winboll/bashPublishAPKAddTag.sh aes
阶段版类库发布命令 git pull &&bash .winboll/bashPublishLIBAddTag.sh libaes
#### 使用说明
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
#### 参考文档

View File

@@ -0,0 +1 @@

48
aes/build.gradle Normal file
View File

@@ -0,0 +1,48 @@
apply plugin: 'com.android.application'
apply from: '../.winboll/winboll_app_build.gradle'
apply from: '../.winboll/winboll_lint_build.gradle'
def genVersionName(def versionName){
// 检查编译标志位配置
assert (winbollBuildProps['stageCount'] != null)
assert (winbollBuildProps['baseVersion'] != null)
// 保存基础版本号
winbollBuildProps.setProperty("baseVersion", "${versionName}");
//保存编译标志配置
FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile)
winbollBuildProps.store(fos, "${winbollBuildPropsDesc}");
fos.close();
// 返回编译版本号
return "${versionName}." + winbollBuildProps['stageCount']
}
android {
// 适配MIUI12
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "cc.winboll.studio.aes"
minSdkVersion 26
targetSdkVersion 30
versionCode 1
// versionName 更新后需要手动设置
// 项目模块目录的 build.gradle 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.20"
if(true) {
versionName = genVersionName("${versionName}")
}
}
// 米盟 SDK
packagingOptions {
doNotStrip "*/*/libmimo_1011.so"
}
}
dependencies {
api project(':libaes')
api fileTree(dir: 'libs', include: ['*.jar'])
}

8
aes/build.properties Normal file
View File

@@ -0,0 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue May 12 13:11:28 HKT 2026
stageCount=4
libraryProject=libaes
baseVersion=15.20
publishVersion=15.20.3
buildCount=0
baseBetaVersion=15.20.4

137
aes/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,137 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# ============================== 基础通用规则 ==============================
# 保留系统组件
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
# 保留 WinBoLL 核心包及子类(合并简化规则)
-keep class cc.winboll.studio.** { *; }
-keepclassmembers class cc.winboll.studio.** { *; }
# 保留所有类中的 public static final String TAG 字段(便于日志定位)
-keepclassmembers class * {
public static final java.lang.String TAG;
}
# 保留序列化类避免Parcelable/Gson解析异常
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# 保留 R 文件避免资源ID混淆
-keepclassmembers class **.R$* {
public static <fields>;
}
# 保留 native 方法避免JNI调用失败
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留注解和泛型(避免反射/序列化异常)
-keepattributes *Annotation*
-keepattributes Signature
# 屏蔽 Java 8+ 警告(适配 Java 7 语法)
-dontwarn java.lang.invoke.*
-dontwarn android.support.v8.renderscript.*
-dontwarn java.util.function.**
# ============================== 第三方框架专项规则 ==============================
# OkHttp 4.4.1米盟广告请求依赖完善Lambda兼容
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-keep class okhttp3.internal.** { *; }
-keep class okio.** { *; }
-dontwarn okhttp3.internal.platform.**
-dontwarn okio.**
# Glide 4.9.0(米盟广告图片加载依赖)
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$ImageType {
**[] $VALUES;
public *;
}
-keepclassmembers class * implements com.bumptech.glide.module.AppGlideModule {
<init>();
}
-dontwarn com.bumptech.glide.**
# Gson 2.8.5(米盟广告数据序列化依赖)
-keep class com.google.gson.** { *; }
-keep interface com.google.gson.** { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
# 米盟 SDK(核心广告组件,完整保留避免加载失败)
-keep class com.miui.zeus.** { *; }
-keep interface com.miui.zeus.** { *; }
# 保留米盟日志字段(便于广告加载失败排查)
-keepclassmembers class com.miui.zeus.mimo.sdk.** {
public static final java.lang.String TAG;
}
# RecyclerView 1.0.0(米盟广告布局渲染依赖)
-keep class androidx.recyclerview.** { *; }
-keep interface androidx.recyclerview.** { *; }
-keepclassmembers class androidx.recyclerview.widget.RecyclerView$Adapter {
public *;
}
# 其他第三方框架(按引入依赖保留,无则可删除)
# XXPermissions 18.63
-keep class com.hjq.permissions.** { *; }
-keep interface com.hjq.permissions.** { *; }
# ZXing 二维码(核心解析组件)
-keep class com.google.zxing.** { *; }
-keep class com.journeyapps.zxing.** { *; }
# Jsoup HTML解析
-keep class org.jsoup.** { *; }
# Pinyin4j 拼音搜索
-keep class net.sourceforge.pinyin4j.** { *; }
# JSch SSH组件
-keep class com.jcraft.jsch.** { *; }
# AndroidX 基础组件
-keep class androidx.appcompat.** { *; }
-keep interface androidx.appcompat.** { *; }
# ============================== 优化与调试配置 ==============================
# 优化级别(平衡混淆效果与性能)
-optimizationpasses 5
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
# 调试辅助(保留行号便于崩溃定位)
-verbose
-dontpreverify
-dontusemixedcaseclassnames
-keepattributes SourceFile,LineNumberTable

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<application>
<!-- Put flavor specific code here -->
</application>
</manifest>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Put flavor specific strings here -->
<string name="app_name">AES+</string>
</resources>

View File

@@ -0,0 +1,45 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.aes">
<!-- 对正在运行的应用重新排序 -->
<uses-permission android:name="android.permission.REORDER_TASKS"/>
<application
android:name=".App"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/MyAESTheme"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<activity android:name=".TestActivityManagerActivity"/>
<activity android:name=".SettingsActivity"/>
<activity android:name=".AboutActivity"/>
</application>
</manifest>

View File

@@ -0,0 +1,78 @@
package cc.winboll.studio.aes;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.aes.R;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.models.APPInfo;
import cc.winboll.studio.libappbase.views.AboutView;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/13 11:25
* @Describe 应用介绍窗口
*/
public class AboutActivity extends BaseWinBoLLActivity {
public static final String TAG = "AboutActivity";
private Toolbar mToolbar;
@Override
public String getTag() {
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
// 设置工具栏
initToolbar();
AboutView aboutView = findViewById(R.id.aboutview);
aboutView.setAPPInfo(genDefaultAppInfo());
}
private void initToolbar() {
LogUtils.d(TAG, "initToolbar() 开始初始化");
mToolbar = findViewById(R.id.toolbar);
if (mToolbar == null) {
LogUtils.e(TAG, "initToolbar() | Toolbar未找到");
return;
}
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag());
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "导航栏 点击返回按钮");
WinBoLLActivityManager.getInstance().resumeActivity(MainActivity.class);
WinBoLLActivityManager.getInstance().finish(AboutActivity.this);
}
});
LogUtils.d(TAG, "initToolbar() 配置完成");
}
private APPInfo genDefaultAppInfo() {
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
String branchName = "aes";
APPInfo appInfo = new APPInfo();
appInfo.setAppName(getString(R.string.app_name));
appInfo.setAppIcon(R.drawable.ic_winboll);
appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("AES");
appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=AES");
appInfo.setAppAPKName("AES");
appInfo.setAppAPKFolderName("AES");
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
return appInfo;
}
}

View File

@@ -0,0 +1,34 @@
package cc.winboll.studio.aes;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/06/13 19:03:58
* @Describe AES应用类
*/
import android.view.Gravity;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
public class App extends GlobalApplication {
public static final String TAG = "App";
@Override
public void onCreate() {
super.onCreate();
setIsDebugging(BuildConfig.DEBUG);
//setIsDebugging(false);
WinBoLLActivityManager.init(this);
// 初始化 Toast 框架
ToastUtils.init(this);
}
@Override
public void onTerminate() {
super.onTerminate();
ToastUtils.release();
}
}

View File

@@ -0,0 +1,45 @@
package cc.winboll.studio.aes;
import android.app.Activity;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.models.AESThemeBean;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/13 16:35
* @Describe BaseWinBollActivity 【继承AppCompatActivity保留核心能力不额外暴露方法】
* 继承链路BaseWinBoLLActivity → AppCompatActivity → FragmentActivityAppCompat能力天然继承可用
*/
public abstract class BaseWinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
public static final String TAG = "BaseWinBoLLActivity";
protected volatile AESThemeBean.ThemeType mThemeType;
@Override
protected void onCreate(Bundle savedInstanceState) {
mThemeType = AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext()));
setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext()));
super.onCreate(savedInstanceState);
WinBoLLActivityManager.getInstance().add(this);
}
@Override
protected void onDestroy() {
WinBoLLActivityManager.getInstance().registeRemove(this);
super.onDestroy();
}
// 子类必须实现getTag(),确保唯一标识
@Override
public abstract String getTag();
@Override
public Activity getActivity() {
return this;
}
}

View File

@@ -0,0 +1,196 @@
package cc.winboll.studio.aes;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/06/13 19:05:52
* @Describe 应用主窗口
*/
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Toast;
import cc.winboll.studio.aes.R;
import cc.winboll.studio.libaes.activitys.DrawerFragmentActivity;
import cc.winboll.studio.libaes.dialogs.LocalFileSelectDialog;
import cc.winboll.studio.libaes.dialogs.StoragePathDialog;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.models.DrawerMenuBean;
import cc.winboll.studio.libaes.unittests.SecondaryLibraryActivity;
import cc.winboll.studio.libaes.unittests.TestAButtonFragment;
import cc.winboll.studio.libaes.unittests.TestASupportToolbarActivity;
import cc.winboll.studio.libaes.unittests.TestAToolbarActivity;
import cc.winboll.studio.libaes.unittests.TestDrawerFragmentActivity;
import cc.winboll.studio.libaes.unittests.TestViewPageFragment;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.LogUtils;
import com.a4455jkjh.colorpicker.ColorPickerDialog;
import java.util.ArrayList;
public class MainActivity extends DrawerFragmentActivity {
public static final String TAG = "MainActivity";
TestAButtonFragment mTestAButtonFragment;
TestViewPageFragment mTestViewPageFragment;
@Override
public String getTag() {
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (mTestAButtonFragment == null) {
mTestAButtonFragment = new TestAButtonFragment();
addFragment(mTestAButtonFragment);
}
showFragment(mTestAButtonFragment);
//setSubtitle(TAG);
//ToastUtils.show("onCreate");
}
@Override
public void initDrawerMenuItemList(ArrayList<DrawerMenuBean> listDrawerMenu) {
super.initDrawerMenuItemList(listDrawerMenu);
LogUtils.d(TAG, "initDrawerMenuItemList");
//listDrawerMenu.clear();
// 添加抽屉菜单项
listDrawerMenu.add(new DrawerMenuBean(R.drawable.ic_launcher, TestAButtonFragment.TAG));
listDrawerMenu.add(new DrawerMenuBean(R.drawable.ic_launcher, TestViewPageFragment.TAG));
notifyDrawerMenuDataChanged();
}
@Override
public void reinitDrawerMenuItemList(ArrayList<DrawerMenuBean> listDrawerMenu) {
super.reinitDrawerMenuItemList(listDrawerMenu);
LogUtils.d(TAG, "reinitDrawerMenuItemList");
//listDrawerMenu.clear();
// 添加抽屉菜单项
listDrawerMenu.add(new DrawerMenuBean(R.drawable.ic_launcher, TestAButtonFragment.TAG));
listDrawerMenu.add(new DrawerMenuBean(R.drawable.ic_launcher, TestViewPageFragment.TAG));
notifyDrawerMenuDataChanged();
}
@Override
public DrawerFragmentActivity.ActivityType initActivityType() {
return DrawerFragmentActivity.ActivityType.Main;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.toolbar_main, menu);
// if(App.isDebugging()) {
// getMenuInflater().inflate(cc.winboll.studio.libaes.R.menu.toolbar_studio_debug, menu);
// }
return super.onCreateOptionsMenu(menu);
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
super.onItemClick(parent, view, position, id);
switch (position) {
case 0 : {
if (mTestAButtonFragment == null) {
mTestAButtonFragment = new TestAButtonFragment();
addFragment(mTestAButtonFragment);
}
showFragment(mTestAButtonFragment);
break;
}
case 1 : {
if (mTestViewPageFragment == null) {
mTestViewPageFragment = new TestViewPageFragment();
addFragment(mTestViewPageFragment);
}
showFragment(mTestViewPageFragment);
break;
}
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int nItemId = item.getItemId();
if (item.getItemId() == R.id.item_testactivitymanager) {
WinBoLLActivityManager.getInstance().startWinBoLLActivity(this, TestActivityManagerActivity.class);
//ToastUtils.show("item_testactivitymanager");
} else
if (nItemId == R.id.item_atoast) {
Toast.makeText(getApplication(), "item_testatoast", Toast.LENGTH_SHORT).show();
} else if (nItemId == R.id.item_atoolbar) {
Intent intent = new Intent(this, TestAToolbarActivity.class);
startActivity(intent);
} else if (nItemId == R.id.item_asupporttoolbar) {
Intent intent = new Intent(this, TestASupportToolbarActivity.class);
startActivity(intent);
} else if (nItemId == R.id.item_colordialog) {
ColorPickerDialog dlg = new ColorPickerDialog(this, getResources().getColor(R.color.colorPrimary));
dlg.setOnColorChangedListener(new com.a4455jkjh.colorpicker.view.OnColorChangedListener() {
@Override
public void beforeColorChanged() {
}
@Override
public void onColorChanged(int color) {
}
@Override
public void afterColorChanged() {
}
});
dlg.show();
} else if (nItemId == R.id.item_dialogstoragepath) {
final StoragePathDialog dialog = new StoragePathDialog(this, 0);
dialog.setOnOKClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
}
});
dialog.show();
} else if (nItemId == R.id.item_localfileselectdialog) {
final LocalFileSelectDialog dialog = new LocalFileSelectDialog(this);
dialog.setOnOKClickListener(new LocalFileSelectDialog.OKClickListener() {
@Override
public void onOKClick(String sz) {
Toast.makeText(getApplication(), sz, Toast.LENGTH_SHORT).show();
//dialog.dismiss();
}
});
dialog.open();
} else if (nItemId == R.id.item_secondarylibraryactivity) {
Intent intent = new Intent(this, SecondaryLibraryActivity.class);
startActivity(intent);
} else if (nItemId == R.id.item_drawerfragmentactivity) {
Intent intent = new Intent(this, TestDrawerFragmentActivity.class);
startActivity(intent);
} else if (nItemId == R.id.item_settings) {
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
} else if (nItemId == R.id.item_about) {
// Intent intent = new Intent(this, AboutActivity.class);
// startActivity(intent);
WinBoLLActivityManager.getInstance().startWinBoLLActivity(this, AboutActivity.class);
}
return super.onOptionsItemSelected(item);
}
}

View File

@@ -0,0 +1,39 @@
package cc.winboll.studio.aes;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import cc.winboll.studio.libaes.views.ADsControlView;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/26 18:01
* @Describe SettingsActivity
*/
public class SettingsActivity extends Activity {
public static final String TAG = "SettingsActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
ADsControlView adsControlView = (ADsControlView) findViewById(R.id.ads_control_view);
// adsControlView.setOnAdsModeSelectedListener(new ADsControlView.OnAdsModeSelectedListener() {
// @Override
// public void onModeSelected(ADsMode selectedMode) {
// if (selectedMode == ADsMode.STANDALONE) {
// // 处理单机模式逻辑(如释放米盟资源)
// ToastUtils.show("STANDALONE");
// } else if (selectedMode == ADsMode.MIMO_SDK) {
// // 处理米盟SDK模式逻辑如初始化SDK
// ToastUtils.show("MIMO_SDK");
// }
// }
// });
}
}

View File

@@ -0,0 +1,33 @@
package cc.winboll.studio.aes;
import android.app.Activity;
import android.os.Bundle;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/09/28 21:07
* @Describe 窗口管理类测试窗口
*/
public class TestActivityManagerActivity extends WinBoLLActivity implements IWinBoLLActivity {
public static final String TAG = "TestActivityManagerActivity";
@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_testactivitymanager);
}
}

View File

@@ -0,0 +1,60 @@
package cc.winboll.studio.aes;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/09/29 00:11
* @Describe WinBoLL 窗口基础类
*/
import android.app.Activity;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.LogUtils;
public class WinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
public static final String TAG = "WinBoLLActivity";
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
@Override
protected void onResume() {
super.onResume();
LogUtils.d(TAG, String.format("onResume %s", getTag()));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
/*if (item.getItemId() == R.id.item_log) {
WinBoLLActivityManager.getInstance().startLogActivity(this);
return true;
} else if (item.getItemId() == R.id.item_home) {
startActivity(new Intent(this, MainActivity.class));
return true;
}*/
// 在switch语句中处理每个ID并在处理完后返回true未处理的情况返回false。
return super.onOptionsItemSelected(item);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
WinBoLLActivityManager.getInstance().add(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
WinBoLLActivityManager.getInstance().finish(this);
}
}

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:left="2dp"
android:top="2dp"
android:right="2dp"
android:bottom="2dp">
<shape android:shape="rectangle" >
<gradient
android:angle="270"
android:endColor="#0F000000"
android:startColor="#0F000000" />
<corners android:radius="?attr/borderCornerRadius" />
</shape>
</item>
<item
android:left="3dp"
android:top="3dp"
android:right="3dp"
android:bottom="5dp">
<shape android:shape="rectangle" >
<gradient
android:angle="270"
android:endColor="#00000000"
android:startColor="#1AFFFFFF" />
<stroke
android:width="1dp"
android:color="#FF666666" />
<corners android:radius="?attr/borderCornerRadius" />
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<!-- 阴影部分 -->
<!-- 个人觉得更形象的表达top代表下边的阴影高度left代表右边的阴影宽度。其实也就是相对应的offsetsolid中的颜色是阴影的颜色也可以设置角度等等 -->
<item
android:left="2dp"
android:top="2dp"
android:right="2dp"
android:bottom="2dp">
<shape android:shape="rectangle" >
<gradient
android:angle="270"
android:endColor="#0F000000"
android:startColor="#0F000000" />
<corners android:radius="?attr/borderCornerRadius" />
</shape>
</item>
<!-- 背景部分 -->
<!-- 形象的表达bottom代表背景部分在上边缘超出阴影的高度right代表背景部分在左边超出阴影的宽度相对应的offset -->
<item
android:left="3dp"
android:top="3dp"
android:right="3dp"
android:bottom="5dp">
<shape android:shape="rectangle" >
<gradient
android:angle="270"
android:endColor="#0FFFFFFF"
android:startColor="#FFFFFFFF" />
<corners android:radius="?attr/borderCornerRadius" />
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" android:background="@drawable/bg_container_border">
<cc.winboll.studio.libaes.views.ASupportToolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/toolbar"/>
<cc.winboll.studio.libappbase.views.AboutView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0"
android:id="@+id/aboutview"/>
</LinearLayout>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" android:background="@drawable/bg_container_border">
<cc.winboll.studio.libaes.views.ADsControlView
android:id="@+id/ads_control_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_frame"
android:padding="10dp"/>
</LinearLayout>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" android:background="@drawable/bg_container_border">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="窗口管理类测试窗口"/>
</LinearLayout>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/item_testactivitymanager"
android:title="TestActivityManager"/>
<item
android:id="@+id/item_log"
android:title="LogActivity"/>
<item
android:id="@+id/item_colordialog"
android:title="ColorDialog"/>
<item
android:id="@+id/item_dialogstoragepath"
android:title="StoragePathDialog"/>
<item
android:id="@+id/item_localfileselectdialog"
android:title="LocalFileSelectDialog"/>
<item
android:id="@+id/item_atoolbar"
android:title="Test AToolbar"/>
<item
android:id="@+id/item_asupporttoolbar"
android:title="Test ASupportToolbar"/>
<item
android:id="@+id/item_atoast"
android:title="Test AToast"/>
<item
android:id="@+id/item_secondarylibraryactivity"
android:title="Test SecondaryLibraryActivity"/>
<item
android:id="@+id/item_drawerfragmentactivity"
android:title="Test DrawerFragmentActivity"/>
<item
android:id="@+id/item_settings"
android:title="Settings"/>
<item
android:id="@+id/item_about"
android:title="About"/>
</menu>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="MyAESTheme" parent="AESTheme">
<item name="themeDebug">@style/MyDebugActivityTheme</item>
</style>
<style name="MyDebugActivityTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:statusBarColor">@color/toolbarBackgroundColor</item>
<item name="colorTittle">@color/mainWindowTextColor</item>
<item name="colorTittleBackgound">@color/toolbarBackgroundColor</item>
<item name="colorText">@color/debugTextColor</item>
<item name="colorTextBackgound">@color/mainWindowBackgroundColor</item>
<item name="debugTextColor">@color/debugTextColor</item>
<item name="toolbarTextColor">@color/toolbarTextColor</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#FF00B322</color>
<color name="colorPrimaryDark">#FF005C12</color>
<color name="colorAccent">#FF8DFFA2</color>
<color name="colorText">#FFFFFB8D</color>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">AES</string>
<string name="app_description">WinBoLL AndroidX 可视化元素类库。</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="MyAESTheme" parent="AESTheme">
<item name="themeDebug">@style/MyDebugActivityTheme</item>
</style>
<style name="MyDebugActivityTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:statusBarColor">@color/toolbarBackgroundColor</item>
<item name="colorTittle">@color/mainWindowTextColor</item>
<item name="colorTittleBackgound">@color/toolbarBackgroundColor</item>
<item name="colorText">@color/debugTextColor</item>
<item name="colorTextBackgound">@color/mainWindowBackgroundColor</item>
<item name="debugTextColor">@color/debugTextColor</item>
<item name="toolbarTextColor">@color/toolbarTextColor</item>
</style>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">winboll.cc</domain>
</domain-config>
</network-security-config>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<application>
<!-- Put flavor specific code here -->
</application>
</manifest>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Put flavor specific strings here -->
</resources>

1
appbase/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

36
appbase/README.md Normal file
View File

@@ -0,0 +1,36 @@
# APPBase
[![](https://jitpack.io/v/ZhanGSKen/APPBase.svg)](https://jitpack.io/#ZhanGSKen/APPBase)
#### 介绍
WinBoLL 安卓手机端安卓应用开发基础类库。
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
#### Gradle 编译说明
调试版编译命令 gradle assembleBetaDebug
阶段版编译命令 bash .winboll/bashPublishAPKAddTag.sh appbase
阶段版类库发布命令 git pull &&bash .winboll/bashPublishLIBAddTag.sh libappbase
#### 使用说明
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
#### 参考文档

View File

@@ -0,0 +1 @@

50
appbase/build.gradle Normal file
View File

@@ -0,0 +1,50 @@
apply plugin: 'com.android.application'
apply from: '../.winboll/winboll_app_build.gradle'
apply from: '../.winboll/winboll_lint_build.gradle'
def genVersionName(def versionName){
// 检查编译标志位配置
assert (winbollBuildProps['stageCount'] != null)
assert (winbollBuildProps['baseVersion'] != null)
// 保存基础版本号
winbollBuildProps.setProperty("baseVersion", "${versionName}");
//保存编译标志配置
FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile)
winbollBuildProps.store(fos, "${winbollBuildPropsDesc}");
fos.close();
// 返回编译版本号
return "${versionName}." + winbollBuildProps['stageCount']
}
android {
// 适配MIUI12
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "cc.winboll.studio.appbase"
minSdkVersion 26
targetSdkVersion 30
versionCode 1
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.20"
if(true) {
versionName = genVersionName("${versionName}")
}
}
// 确保 Java 7 兼容性(已适配项目技术栈)
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
}
dependencies {
api project(':libappbase')
api fileTree(dir: 'libs', include: ['*.jar'])
}

8
appbase/build.properties Normal file
View File

@@ -0,0 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue May 12 09:17:15 HKT 2026
stageCount=10
libraryProject=libappbase
baseVersion=15.20
publishVersion=15.20.9
buildCount=0
baseBetaVersion=15.20.10

126
appbase/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,126 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# ============================== 基础通用规则 ==============================
# 保留系统组件
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
# 保留 WinBoLL 核心包及子类(适配你的两个包名)
#-keep public class * extends com.winboll.WinBoLLActivity
#-keep public class * extends com.winboll.WinBoLLFragment
# 主包名
-keep class cc.winboll.studio.*.** { *; }
# beta包名
-keep class cc.winboll.studio.*.beta.** { *; }
-keepclassmembers class cc.winboll.studio.*.** { *; }
-keepclassmembers class cc.winboll.studio.*.beta.** { *; }
# 保留所有类中的 public static final String TAG 字段
-keepclassmembers class * {
public static final java.lang.String TAG;
}
# 保留序列化类
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# 保留 R 文件
-keepclassmembers class **.R$* {
public static <fields>;
}
# 保留 native 方法
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留注解和泛型
-keepattributes *Annotation*
-keepattributes Signature
# 屏蔽 Java 8+ 警告(适配 Java 7
-dontwarn java.lang.invoke.*
-dontwarn android.support.v8.renderscript.*
-dontwarn java.util.function.**
# ============================== 第三方框架规则 ==============================
# Retrofit + OkHttp
-keep class retrofit2.** { *; }
-keep interface retrofit2.** { *; }
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-keep class okio.** { *; }
-keepclasseswithmembers class * {
@retrofit2.http.* <methods>;
}
# Glide 4.x
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$ImageType {
**[] $VALUES;
public *;
}
-dontwarn com.bumptech.glide.load.resource.bitmap.VideoDecoder
# GreenDAO 3.x
-keepclassmembers class * extends org.greenrobot.greendao.AbstractDao {
public static java.lang.String TABLENAME;
}
-keep class **$Properties
# 实体类包名(按实际调整)
#-keep class cc.winboll.studio.appbase.model.** { *; }
# ButterKnife 8.x
-keep class butterknife.** { *; }
-dontwarn butterknife.internal.**
-keep class **$$ViewBinder { *; }
-keepclasseswithmembernames class * {
@butterknife.BindView <fields>;
@butterknife.OnClick <methods>;
}
# EventBus 3.x
-keepclassmembers class ** {
@org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
# ============================== 优化与调试 ==============================
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
-optimizationpasses 5
-verbose
-dontpreverify
-dontusemixedcaseclassnames
# 保留行号(便于崩溃定位)
-keepattributes SourceFile,LineNumberTable

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<application
tools:replace="android:icon"
android:icon="@drawable/ic_winboll_beta">
<!-- Put flavor specific code here -->
</application>
</manifest>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">APPBase+</string>
</resources>

View File

@@ -0,0 +1,57 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.appbase">
<application
android:name=".App"
android:icon="@drawable/ic_winboll"
android:label="@string/app_name"
android:theme="@style/MyAPPBaseTheme"
android:resizeableActivity="true"
android:process=":App"
android:sharedUserId="@string/shared_user_id"
android:sharedUserLabel="@string/shared_user_label">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:exported="true"
android:resizeableActivity="true"
android:launchMode="singleTop"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
android:name=".Main2Activity"
android:label="@string/app_name"
android:exported="true"
android:resizeableActivity="true"
android:launchMode="singleTop"
android:taskAffinity="cc.winboll.studio.appbase.Main2Activity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation">
</activity>
<activity android:name=".GlobalApplication$CrashActivity"/>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<activity android:name="cc.winboll.studio.libappbase.activities.CrashCopyReceiverActivity"/>
<activity android:name=".AboutActivity"/>
</application>
</manifest>

View File

@@ -0,0 +1,50 @@
package cc.winboll.studio.appbase;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toolbar;
import cc.winboll.studio.appbase.R;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.models.APPInfo;
import cc.winboll.studio.libappbase.views.AboutView;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/11 12:55
* @Describe AboutActivity
*/
public class AboutActivity extends Activity {
public static final String TAG = "AboutActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
Toolbar toolbar = findViewById(R.id.toolbar);
setActionBar(toolbar);
AboutView aboutView = findViewById(R.id.aboutview);
aboutView.setAPPInfo(genDefaultAppInfo());
}
private APPInfo genDefaultAppInfo() {
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
String branchName = "appbase";
APPInfo appInfo = new APPInfo();
appInfo.setAppName("APPBase");
appInfo.setAppIcon(R.drawable.ic_winboll);
appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("WinBoLL");
appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=APPBase");
appInfo.setAppAPKName("APPBase");
appInfo.setAppAPKFolderName("APPBase");
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
return appInfo;
}
}

View File

@@ -0,0 +1,48 @@
package cc.winboll.studio.appbase;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.BuildConfig;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/01/05 09:54:42
* @Describe 应用全局入口类(继承基础库 GlobalApplication
* 负责应用初始化、全局资源管理与生命周期回调处理,是整个应用的核心入口
*/
public class App extends GlobalApplication {
/** 当前应用类的日志 TAG用于调试输出标识日志来源 */
public static final String TAG = "App";
/**
* 应用创建时回调(全局初始化入口)
* 在应用进程启动时执行,仅调用一次,用于初始化全局工具类、第三方库等
*/
@Override
public void onCreate() {
super.onCreate();
// 如果应用不在调试状态,就根据编译类型设置调试状态
if (isDebugging() != true) {
setIsDebugging(BuildConfig.DEBUG);
}
// release 版调试码
//setIsDebugging(!BuildConfig.DEBUG);
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
ToastUtils.init(getApplicationContext());
}
/**
* 应用终止时回调(资源释放入口)
* 仅在模拟环境(如 Android Studio 模拟器)中可靠触发,真机上可能因系统回收进程不执行
* 用于释放全局资源,避免内存泄漏
*/
@Override
public void onTerminate() {
super.onTerminate(); // 调用父类终止逻辑(如基础库资源释放)
// 释放 Toast 工具类资源(销毁全局 Toast 实例,避免内存泄漏)
ToastUtils.release();
}
}

View File

@@ -0,0 +1,28 @@
package cc.winboll.studio.appbase;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
public class CrashTestActivity extends Activity {
public static final String TAG = "CrashTestActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_crash_test);
LogUtils.d(TAG, "CrashTestActivity onCreate()");
}
public void onBack(View view) {
finish();
}
public void onTestCrash(View view) {
LogUtils.d(TAG, "onTestCrash()");
ToastUtils.show("测试布局崩溃...");
}
}

View File

@@ -0,0 +1,20 @@
package cc.winboll.studio.appbase;
import android.os.Bundle;
import android.widget.Toolbar;
import cc.winboll.studio.appbase.R;
public class Main2Activity extends MainActivity {
public static final String TAG = "Main2Activity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
Toolbar toolbar = findViewById(R.id.toolbar);
if (toolbar != null) {
setActionBar(toolbar);
}
}
}

View File

@@ -0,0 +1,177 @@
package cc.winboll.studio.appbase;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toolbar;
import cc.winboll.studio.appbase.R;
import cc.winboll.studio.appbase.model.TestBean;
import cc.winboll.studio.libappbase.LogActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 未标注(建议补充创建日期)
* @Describe 应用主界面 Activity入口界面
* 包含功能测试按钮崩溃测试、日志查看、Toast测试、顶部工具栏菜单功能是应用交互的核心入口
*/
public class MainActivity extends Activity {
/** 当前 Activity 的日志 TAG用于调试输出标识日志来源 */
public static final String TAG = "MainActivity";
/** 顶部工具栏(用于展示标题、菜单,绑定布局中的 Toolbar 控件) */
private Toolbar mToolbar;
/**
* Activity 创建时回调(初始化界面)
* 在 Activity 首次创建时执行,用于加载布局、初始化控件、设置事件监听
* @param savedInstanceState 保存 Activity 状态的 Bundle如屏幕旋转时的数据恢复
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//ToastUtils.show("onCreate"); // 显示 Activity 创建提示(调试用)
setContentView(R.layout.activity_main); // 加载主界面布局
// 初始化 Toolbar 并设置为 ActionBar
mToolbar = findViewById(R.id.toolbar);
setActionBar(mToolbar); // 将 Toolbar 替代系统默认 ActionBar
initTestData();
}
void initTestData() {
TestBean bean1 = new TestBean();
bean1.setTestNum1(456);
TestBean.saveBeanToFile(getFilesDir().getAbsolutePath() + getTestBeanRelativePath(), bean1);
TestBean bean2 = new TestBean();
bean2.setTestNum1(789);
TestBean.saveBeanToFile(getExternalFilesDir(null).getAbsolutePath() + getTestBeanRelativePath(), bean2);
}
String getTestBeanRelativePath() {
return "/BaseBaen/"+TestBean.class.getName()+".json";
}
/**
* 创建菜单时回调(加载工具栏菜单)
* 初始化 ActionBar 菜单,加载自定义菜单布局
* @param menu 菜单对象(用于承载菜单项)
* @return true显示菜单false不显示菜单
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// 加载菜单布局R.menu.toolbar_main 为自定义菜单文件)
getMenuInflater().inflate(R.menu.toolbar_main, menu);
return super.onCreateOptionsMenu(menu);
}
/**
* 菜单 item 点击时回调(处理菜单事件)
* 响应 Toolbar 菜单项的点击事件,执行对应业务逻辑
* @param item 被点击的菜单项
* @return true消费点击事件false不消费传递给父类
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.item_home:
// 点击 "首页/官网" 菜单项,唤起浏览器打开指定网站
openWebsiteInBrowser(this);
break;
// 可扩展其他菜单项(如设置、关于等)的处理逻辑
}
return super.onOptionsItemSelected(item);
}
/**
* 崩溃测试按钮点击事件(触发应用崩溃,用于调试异常捕获)
* 故意执行非法操作(循环获取不存在的字符串资源),强制应用崩溃
* @param view 触发事件的 View对应布局中的崩溃测试按钮
*/
public void onCrashTest(View view) {
// 循环从 Integer.MIN_VALUE 到 Integer.MAX_VALUE获取不存在的字符串资源 ID触发崩溃
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
getString(i); // i 超出资源 ID 范围,抛出 Resources.NotFoundException 导致崩溃
}
}
public void onLogTestNewTask(View view) {
LogActivity.startLogActivity(this, true);
}
/**
* 日志测试按钮点击事件(打开日志查看界面)
* 启动 LogActivity用于查看应用运行日志
* @param view 触发事件的 View对应布局中的日志测试按钮
*/
public void onLogTest(View view) {
LogActivity.startLogActivity(this, false);
}
/**
* Toast 工具测试按钮点击事件(测试全局 Toast 功能)
* 测试主线程、子线程中 Toast 的显示效果,验证 ToastUtils 的可用性
* @param view 触发事件的 View对应布局中的 Toast 测试按钮)
*/
public void onToastUtilsTest(View view) {
LogUtils.d(TAG, "onToastUtilsTest"); // 打印调试日志,标识进入 Toast 测试
ToastUtils.show("Hello, WinBoLL!"); // 主线程显示 Toast
// 开启子线程,延迟 2 秒后显示 Toast测试子线程 Toast 兼容性)
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000); // 线程休眠 2 秒
// 若 ToastUtils 已处理主线程切换,此处可直接调用;否则需通过 Handler 切换到主线程
ToastUtils.show("Thread.sleep(2000);ToastUtils.show...");
} catch (InterruptedException e) {
// 捕获线程中断异常(如线程被销毁时),不做处理(测试场景)
e.printStackTrace();
}
}
}).start();
}
/**
* 唤起系统默认浏览器打开指定网站(跳转至应用官网)
* 通过 Intent.ACTION_VIEW 隐式意图,触发浏览器打开目标 URL
* @param context 上下文对象(如 Activity、Application此处为 MainActivity
*/
public void openWebsiteInBrowser(Context context) {
String url = "https://www.winboll.cc"; // 目标网站 URL应用官网
// 构建隐式意图ACTION_VIEW 表示查看指定数据Uri 为网站地址)
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
// 设置标志:在新的任务栈中启动 Activity避免与当前应用任务栈混淆
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// 启动意图(唤起浏览器)
context.startActivity(intent);
}
public void onAboutActivity(View view) {
LogUtils.d(TAG, "onAboutActivity() 调用");
Intent aboutIntent = new Intent(getApplicationContext(), AboutActivity.class);
startActivity(aboutIntent);
}
public void onMultiInstance(View view) {
LogUtils.d(TAG, "onMultiInstance() 多开窗口按钮已点击");
ToastUtils.show("多开窗口:已启动新窗口");
android.content.Intent intent = new android.content.Intent(this, Main2Activity.class);
intent.setFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
LogUtils.d(TAG, "onMultiInstance() 准备启动Main2Activity");
startActivity(intent);
LogUtils.d(TAG, "onMultiInstance() Main2Activity已启动");
}
}

View File

@@ -0,0 +1,154 @@
package cc.winboll.studio.appbase.model;
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.IOException;
/**
* 测试实体类
* 继承BaseBean实现JSON序列化/反序列化能力提供基础int类型属性的封装与数据持久化支持
* 适配Java7语法遵循BaseBean统一的反射识别、JSON读写规范
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/31 19:16:00
* @LastEditTime 2026/02/01 10:46:00
*/
public class TestBean extends BaseBean {
// ====================================== 常量定义 ======================================
/** 当前类的日志 TAG用于调试输出 */
public static final String TAG = "TestBean";
// ====================================== 成员属性 ======================================
/**
* 测试数字属性默认值123
* 基础int类型属性用于测试BaseBean的JSON序列化/反序列化能力
*/
private int testNum1;
// ====================================== 构造方法 ======================================
/**
* 无参构造器(默认初始化)
* 给testNum1赋值默认值123满足反射实例化、JSON解析的无参构造要求
*/
public TestBean() {
this.testNum1 = 123;
LogUtils.d(TAG, "TestBean无参构造器调用testNum1默认初始化值" + this.testNum1);
}
/**
* 有参构造器(自定义初始化)
* @param testNum1 测试数字初始值
*/
public TestBean(int testNum1) {
this.testNum1 = testNum1;
LogUtils.d(TAG, "TestBean有参构造器调用传入testNum1" + testNum1);
}
// ====================================== Get/Set 方法 ======================================
/**
* 设置测试数字属性值
* @param testNum1 待设置的int类型值
*/
public void setTestNum1(int testNum1) {
LogUtils.d(TAG, "setTestNum1调用传入参数" + testNum1);
this.testNum1 = testNum1;
}
/**
* 获取测试数字属性值
* @return 当前testNum1的int类型值
*/
public int getTestNum1() {
LogUtils.d(TAG, "getTestNum1调用返回值" + this.testNum1);
return testNum1;
}
// ====================================== 重写父类BaseBean方法 ======================================
/**
* 重写父类方法:获取当前类的全限定名
* 用于BaseBean反射识别、类名匹配等统一逻辑
* @return 类全限定名cc.winboll.studio.appbase.model.TestBean
*/
@Override
public String getName() {
LogUtils.d(TAG, "getName方法调用返回类全限定名" + TestBean.class.getName());
return TestBean.class.getName();
}
/**
* 重写父类方法将当前对象序列化为JSON持久化存储专用
* 遵循BaseBean规范先执行父类序列化逻辑再处理子类专属字段
* @param jsonWriter JSON写入器外部传入的JSON流操作实例
* @throws IOException JSON写入异常流关闭、格式错误等
*/
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
LogUtils.d(TAG, "writeThisToJsonWriter调用传入参数JsonWriter" + jsonWriter);
// 执行父类公共字段的序列化逻辑
super.writeThisToJsonWriter(jsonWriter);
// 序列化子类专属字段testNum1
jsonWriter.name("testNum1").value(this.getTestNum1());
LogUtils.d(TAG, "writeThisToJsonWriter执行完成已序列化testNum1" + this.getTestNum1());
}
/**
* 重写父类方法从JSON字段初始化当前对象属性解析JSON专用
* 先让父类处理公共字段再匹配子类专属字段不匹配则返回false跳过
* @param jsonReader JSON读取器外部传入的JSON流操作实例
* @param name 当前解析的JSON字段名
* @return true-字段解析成功false-字段不匹配,需跳过/父类处理
* @throws IOException JSON读取异常字段类型不匹配、流中断等
*/
@Override
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
LogUtils.d(TAG, "initObjectsFromJsonReader调用传入参数name=" + name + "JsonReader=" + jsonReader);
// 父类优先处理公共字段,处理成功则直接返回
if (super.initObjectsFromJsonReader(jsonReader, name)) {
LogUtils.d(TAG, "initObjectsFromJsonReader字段" + name + "由父类BaseBean处理成功");
return true;
}
// 解析子类专属字段
if ("testNum1".equals(name)) {
this.setTestNum1(jsonReader.nextInt());
LogUtils.d(TAG, "initObjectsFromJsonReader解析testNum1成功值为" + this.getTestNum1());
} else {
LogUtils.w(TAG, "initObjectsFromJsonReader字段" + name + "不匹配返回false跳过解析");
// 字段不匹配返回false表示跳过
return false;
}
return true;
}
/**
* 重写父类方法从JSON读取器完整解析并初始化当前对象JSON解析入口
* 负责JSON对象的开始/结束标识处理,遍历所有字段并调用字段解析方法
* 严格遵循writeThisToJsonWriter的序列化结构保证解析一致性
* @param jsonReader JSON读取器外部传入的JSON流操作实例
* @return 解析后的当前TestBean实例支持链式调用
* @throws IOException JSON解析异常格式错误、字段缺失、流异常等
*/
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
LogUtils.d(TAG, "readBeanFromJsonReader调用传入参数JsonReader" + jsonReader);
// 开始解析JSON对象与序列化结构保持一致
jsonReader.beginObject();
// 遍历所有JSON字段
while (jsonReader.hasNext()) {
String fieldName = jsonReader.nextName();
LogUtils.d(TAG, "readBeanFromJsonReader开始解析字段fieldName=" + fieldName);
// 解析字段,不匹配则跳过该值
if (!this.initObjectsFromJsonReader(jsonReader, fieldName)) {
jsonReader.skipValue();
LogUtils.w(TAG, "readBeanFromJsonReader字段" + fieldName + "解析失败,已跳过该值");
}
}
// 结束JSON对象解析必须调用避免流异常
jsonReader.endObject();
LogUtils.d(TAG, "readBeanFromJsonReader执行完成JSON解析结束当前TestBean实例testNum1" + this.getTestNum1());
// 返回当前实例,支持链式调用
return this;
}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@android:color/transparent" />
<stroke android:width="1dp" android:color="#FFB0B0B0" />
<corners android:radius="?attr/borderCornerRadius" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#81C7F5"/> <!-- 浅蓝色填充 -->
<corners android:radius="8dp"/> <!-- 8dp 圆角 -->
</shape>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/activityBackgroundColor">
<android.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/toolbarBackgroundColor"
android:id="@+id/toolbar"/>
<cc.winboll.studio.libappbase.views.AboutView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0"
android:id="@+id/aboutview"/>
</LinearLayout>

View File

@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="0dp"
android:background="?attr/activityBackgroundColor">
<android.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/toolbarBackgroundColor"
android:id="@+id/toolbar"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_vertical"
android:spacing="12dp">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="关于应用"
android:textSize="16sp"
android:textColor="?attr/activityTextColor"
android:background="?attr/toolbarBackgroundColor"
android:paddingVertical="12dp"
android:layout_marginHorizontal="24dp"
android:onClick="onAboutActivity"
android:layout_margin="10dp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="应用崩溃测试"
android:textSize="16sp"
android:textColor="?attr/activityTextColor"
android:background="?attr/toolbarBackgroundColor"
android:paddingVertical="12dp"
android:layout_marginHorizontal="24dp"
android:onClick="onCrashTest"
android:layout_margin="10dp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="应用日志测试"
android:textSize="16sp"
android:textColor="?attr/activityTextColor"
android:background="?attr/toolbarBackgroundColor"
android:paddingVertical="12dp"
android:layout_marginHorizontal="24dp"
android:onClick="onLogTest"
android:layout_margin="10dp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="应用日志测试(新窗口)"
android:textSize="16sp"
android:textColor="?attr/activityTextColor"
android:background="?attr/toolbarBackgroundColor"
android:paddingVertical="12dp"
android:layout_marginHorizontal="24dp"
android:onClick="onLogTestNewTask"
android:layout_margin="10dp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="应用吐司测试"
android:textSize="16sp"
android:textColor="?attr/activityTextColor"
android:background="?attr/toolbarBackgroundColor"
android:paddingVertical="12dp"
android:layout_marginHorizontal="24dp"
android:onClick="onToastUtilsTest"
android:layout_margin="10dp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="多开窗口"
android:textSize="16sp"
android:textColor="?attr/activityTextColor"
android:background="?attr/toolbarBackgroundColor"
android:paddingVertical="12dp"
android:layout_marginHorizontal="24dp"
android:onClick="onMultiInstance"
android:layout_margin="10dp"/>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:background="@android:color/white">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Main2Activity"
android:textSize="24sp"
android:textColor="@color/gray_900"/>
</LinearLayout>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/activityBackgroundColor">
<android.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/toolbarBackgroundColor"
android:id="@+id/toolbar"/>
<cc.winboll.studio.libappbase.views.AboutView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0"
android:id="@+id/aboutview"/>
</LinearLayout>

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="0dp"
android:background="?attr/activityBackgroundColor">
<android.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/toolbarBackgroundColor"
android:id="@+id/toolbar"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_vertical">
<cc.winboll.studio.appbase.UndefinedCustomView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="返回"
android:textSize="16sp"
android:textColor="?attr/activityTextColor"
android:background="?attr/toolbarBackgroundColor"
android:paddingVertical="12dp"
android:layout_marginHorizontal="24dp"
android:onClick="onBack"
android:layout_margin="10dp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="测试崩溃"
android:textSize="16sp"
android:textColor="?attr/activityTextColor"
android:background="?attr/toolbarBackgroundColor"
android:paddingVertical="12dp"
android:layout_marginHorizontal="24dp"
android:onClick="onTestCrash"
android:layout_margin="10dp"/>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="0dp"
android:background="?attr/activityBackgroundColor">
<android.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/toolbarBackgroundColor"
android:id="@+id/toolbar"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_vertical"
android:spacing="12dp">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="关于应用"
android:textSize="16sp"
android:textColor="?attr/activityTextColor"
android:background="?attr/toolbarBackgroundColor"
android:paddingVertical="12dp"
android:layout_marginHorizontal="24dp"
android:onClick="onAboutActivity"
android:layout_margin="10dp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="应用崩溃测试"
android:textSize="16sp"
android:textColor="?attr/activityTextColor"
android:background="?attr/toolbarBackgroundColor"
android:paddingVertical="12dp"
android:layout_marginHorizontal="24dp"
android:onClick="onCrashTest"
android:layout_margin="10dp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="应用日志测试"
android:textSize="16sp"
android:textColor="?attr/activityTextColor"
android:background="?attr/toolbarBackgroundColor"
android:paddingVertical="12dp"
android:layout_marginHorizontal="24dp"
android:onClick="onLogTest"
android:layout_margin="10dp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="应用日志测试(新窗口)"
android:textSize="16sp"
android:textColor="?attr/activityTextColor"
android:background="?attr/toolbarBackgroundColor"
android:paddingVertical="12dp"
android:layout_marginHorizontal="24dp"
android:onClick="onLogTestNewTask"
android:layout_margin="10dp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="应用吐司测试"
android:textSize="16sp"
android:textColor="?attr/activityTextColor"
android:background="?attr/toolbarBackgroundColor"
android:paddingVertical="12dp"
android:layout_marginHorizontal="24dp"
android:onClick="onToastUtilsTest"
android:layout_margin="10dp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="多开窗口"
android:textSize="16sp"
android:textColor="?attr/activityTextColor"
android:background="?attr/toolbarBackgroundColor"
android:paddingVertical="12dp"
android:layout_marginHorizontal="24dp"
android:onClick="onMultiInstance"
android:layout_margin="10dp"/>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:background="?attr/activityBackgroundColor">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Main2Activity"
android:textSize="24sp"
android:textColor="?attr/activityTextColor"/>
</LinearLayout>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/item_home"
android:title="Home"
android:icon="@drawable/ic_winboll"
android:showAsAction="always"/>
</menu>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#FF1B8B29</color>
<color name="colorPrimaryDark">#FF0A5520</color>
<color name="colorAccent">#FF6EE87C</color>
<color name="colorText">#FFB8FF7D</color>
</resources>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="MyAPPBaseTheme" parent="APPBaseTheme">
<item name="themeDebug">@style/MyDebugActivityTheme</item>
</style>
<style name="MyDebugActivityTheme" parent="DebugActivityTheme">
<item name="colorTittle">?attr/mainWindowDarkTextColor</item>
<item name="colorTittleBackgound">?attr/toolbarBackgroundColor</item>
<item name="colorText">?attr/debugTextColor</item>
<item name="colorTextBackgound">?attr/mainWindowDarkBackgroundColor</item>
<item name="toolbarTextColor">@color/toolbarTextColor</item>
</style>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="borderCornerRadius" format="dimension" />
<declare-styleable name="AboutView">
<attr name="app_name" format="string" />
<attr name="app_apkfoldername" format="string" />
<attr name="app_apkname" format="string" />
<attr name="app_gitname" format="string" />
<attr name="app_gitowner" format="string" />
<attr name="app_gitappbranch" format="string" />
<attr name="app_gitappsubprojectfolder" format="string" />
<attr name="appdescription" format="string" />
<attr name="appicon" format="reference" />
<attr name="is_adddebugtools" format="boolean" />
</declare-styleable>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#FF00B322</color>
<color name="colorPrimaryDark">#FF005C12</color>
<color name="colorAccent">#FF8DFFA2</color>
<color name="colorText">#FFFFFB8D</color>
</resources>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">APPBase</string>
<string name="app_description">WinBoLL 安卓手机端安卓应用开发基础类库。</string>
<string name="app_normal">Click here is switch to Normal APP</string>
<string name="app_debug">Click here is switch to APP DEBUG</string>
<string name="gitea_home">GITEA HOME</string>
<string name="app_update">APP UPDATE</string>
</resources>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="MyAPPBaseTheme" parent="APPBaseTheme">
<item name="themeDebug">@style/MyDebugActivityTheme</item>
</style>
<style name="MyDebugActivityTheme" parent="DebugActivityTheme">
<item name="colorTittle">?attr/mainWindowTextColor</item>
<item name="colorTittleBackgound">?attr/toolbarBackgroundColor</item>
<item name="colorText">?attr/debugTextColor</item>
<item name="colorTextBackgound">?attr/mainWindowBackgroundColor</item>
<item name="toolbarTextColor">@color/toolbarTextColor</item>
</style>
</resources>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<application>
<!-- Put flavor specific code here -->
</application>
</manifest>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Put flavor specific strings here -->
</resources>

1
autonfc/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

34
autonfc/README.md Normal file
View File

@@ -0,0 +1,34 @@
# AutoNFC
#### 介绍
NFC 卡应用,主要管理 NFC 卡接触手机的动作响应NFC 接触状态用于作为其他应用激活活动动作的启动令牌。
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
#### Gradle 编译说明
调试版编译命令 gradle assembleBetaDebug
阶段版编译命令 bash .winboll/bashPublishAPKAddTag.sh autonfc
#### 使用说明
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
#### 参考文档

View File

@@ -0,0 +1 @@

119
autonfc/build.gradle Normal file
View File

@@ -0,0 +1,119 @@
apply plugin: 'com.android.application'
apply from: '../.winboll/winboll_app_build.gradle'
apply from: '../.winboll/winboll_lint_build.gradle'
def genVersionName(def versionName){
// 检查编译标志位配置
assert (winbollBuildProps['stageCount'] != null)
assert (winbollBuildProps['baseVersion'] != null)
// 保存基础版本号
winbollBuildProps.setProperty("baseVersion", "${versionName}");
//保存编译标志配置
FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile)
winbollBuildProps.store(fos, "${winbollBuildPropsDesc}");
fos.close();
// 返回编译版本号
return "${versionName}." + winbollBuildProps['stageCount']
}
android {
// 适配MIUI12
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "cc.winboll.studio.autonfc"
minSdkVersion 23
// 适配MIUI12
targetSdkVersion 30
versionCode 1
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.11"
if(true) {
versionName = genVersionName("${versionName}")
}
}
// 米盟 SDK
packagingOptions {
doNotStrip "*/*/libmimo_1011.so"
}
sourceSets {
main {
jniLibs.srcDirs = ['libs'] // 若SO库放在libs目录下
}
}
}
dependencies {
api 'com.google.code.gson:gson:2.10.1'
// 下拉控件
api 'com.baoyz.pullrefreshlayout:library:1.2.0'
// SSH
api 'com.jcraft:jsch:0.1.55'
// Html 解析
api 'org.jsoup:jsoup:1.13.1'
// 二维码类库
api 'com.google.zxing:core:3.4.1'
api 'com.journeyapps:zxing-android-embedded:3.6.0'
// 应用介绍页类库
api 'io.github.medyo:android-about-page:2.0.0'
// 网络连接类库
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 类库
/*api 'androidx.appcompat:appcompat:1.1.0'
//api 'com.google.android.material:material:1.4.0'
//api 'androidx.viewpager:viewpager:1.0.0'
//api 'androidx.vectordrawable:vectordrawable:1.1.0'
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
//api 'androidx.fragment:fragment:1.1.0'*/
// 米盟
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
//注意以下5个库必须要引入
//implementation 'androidx.appcompat:appcompat:1.4.1'
api 'androidx.recyclerview:recyclerview:1.0.0'
api 'com.google.code.gson:gson:2.8.5'
api 'com.github.bumptech.glide:glide:4.9.0'
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
implementation "androidx.annotation:annotation:1.3.0"
implementation "androidx.core:core:1.6.0"
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
implementation "androidx.preference:preference:1.1.1"
implementation "androidx.viewpager:viewpager:1.0.0"
implementation "com.google.android.material:material:1.4.0"
implementation "com.google.guava:guava:24.1-jre"
/*
implementation "io.noties.markwon:core:$markwonVersion"
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
implementation "io.noties.markwon:linkify:$markwonVersion"
implementation "io.noties.markwon:recycler:$markwonVersion"
*/
implementation 'com.termux:terminal-emulator:0.118.0'
implementation 'com.termux:terminal-view:0.118.0'
implementation 'com.termux:termux-shared:0.118.0'
// WinBoLL库 nexus.winboll.cc 地址
api 'cc.winboll.studio:libaes:15.15.2'
api 'cc.winboll.studio:libappbase:15.15.11'
// WinBoLL备用库 jitpack.io 地址
//api 'com.github.ZhanGSKen:AES:aes-v15.15.7'
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.15.4'
api fileTree(dir: 'libs', include: ['*.jar'])
}

8
autonfc/build.properties Normal file
View File

@@ -0,0 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Mon Mar 16 18:30:19 GMT 2026
stageCount=0
libraryProject=
baseVersion=15.11
publishVersion=15.0.0
buildCount=54
baseBetaVersion=15.0.1

21
autonfc/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<application>
<!-- Put flavor specific code here -->
</application>
</manifest>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">AutoNFC✌</string>
</resources>

View File

@@ -0,0 +1,51 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.autonfc">
<uses-permission android:name="android.permission.NFC"/>
<uses-feature
android:name="android.hardware.nfc"
android:required="true"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:theme="@style/MyAppTheme"
android:resizeableActivity="true"
android:name=".App">
<activity
android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".nfc.NFCInterfaceActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="*/*"/>
</intent-filter>
</activity>
<!-- NFC 绑定服务 -->
<service
android:name=".nfc.AutoNFCService"
android:exported="false"/>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
</application>
</manifest>

View File

@@ -0,0 +1,344 @@
package cc.winboll.studio.autonfc;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.Thread.UncaughtExceptionHandler;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
public class App extends GlobalApplication {
private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
@Override
public void onCreate() {
super.onCreate();
// 初始化 Toast 框架
// ToastUtils.init(this);
// // 设置 Toast 布局样式
// //ToastUtils.setView(R.layout.view_toast);
// ToastUtils.setStyle(new WhiteToastStyle());
// ToastUtils.setGravity(Gravity.BOTTOM, 0, 200);
//
//CrashHandler.getInstance().registerGlobal(this);
//CrashHandler.getInstance().registerPart(this);
}
public static void write(InputStream input, OutputStream output) throws IOException {
byte[] buf = new byte[1024 * 8];
int len;
while ((len = input.read(buf)) != -1) {
output.write(buf, 0, len);
}
}
public static void write(File file, byte[] data) throws IOException {
File parent = file.getParentFile();
if (parent != null && !parent.exists()) parent.mkdirs();
ByteArrayInputStream input = new ByteArrayInputStream(data);
FileOutputStream output = new FileOutputStream(file);
try {
write(input, output);
} finally {
closeIO(input, output);
}
}
public static String toString(InputStream input) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
write(input, output);
try {
return output.toString("UTF-8");
} finally {
closeIO(input, output);
}
}
public static void closeIO(Closeable... closeables) {
for (Closeable closeable : closeables) {
try {
if (closeable != null) closeable.close();
} catch (IOException ignored) {}
}
}
public static class CrashHandler {
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
private static CrashHandler sInstance;
private PartCrashHandler mPartCrashHandler;
public static CrashHandler getInstance() {
if (sInstance == null) {
sInstance = new CrashHandler();
}
return sInstance;
}
public void registerGlobal(Context context) {
registerGlobal(context, null);
}
public void registerGlobal(Context context, String crashDir) {
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir));
}
public void unregister() {
Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER);
}
public void registerPart(Context context) {
unregisterPart(context);
mPartCrashHandler = new PartCrashHandler(context.getApplicationContext());
MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler);
}
public void unregisterPart(Context context) {
if (mPartCrashHandler != null) {
mPartCrashHandler.isRunning.set(false);
mPartCrashHandler = null;
}
}
private static class PartCrashHandler implements Runnable {
private final Context mContext;
public AtomicBoolean isRunning = new AtomicBoolean(true);
public PartCrashHandler(Context context) {
this.mContext = context;
}
@Override
public void run() {
while (isRunning.get()) {
try {
Looper.loop();
} catch (final Throwable e) {
e.printStackTrace();
if (isRunning.get()) {
MAIN_HANDLER.post(new Runnable(){
@Override
public void run() {
Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show();
}
});
} else {
if (e instanceof RuntimeException) {
throw (RuntimeException)e;
} else {
throw new RuntimeException(e);
}
}
}
}
}
}
private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler {
private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss");
private final Context mContext;
private final File mCrashDir;
public UncaughtExceptionHandlerImpl(Context context, String crashDir) {
this.mContext = context;
this.mCrashDir = TextUtils.isEmpty(crashDir) ? new File(mContext.getExternalCacheDir(), "crash") : new File(crashDir);
}
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
try {
String log = buildLog(throwable);
writeLog(log);
try {
Intent intent = new Intent(mContext, CrashActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Intent.EXTRA_TEXT, log);
mContext.startActivity(intent);
} catch (Throwable e) {
e.printStackTrace();
writeLog(e.toString());
}
throwable.printStackTrace();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
} catch (Throwable e) {
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
}
}
private String buildLog(Throwable throwable) {
String time = DATE_FORMAT.format(new Date());
String versionName = "unknown";
long versionCode = 0;
try {
PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
versionName = packageInfo.versionName;
versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
} catch (Throwable ignored) {}
LinkedHashMap<String, String> head = new LinkedHashMap<String, String>();
head.put("Time Of Crash", time);
head.put("Device", String.format("%s, %s", Build.MANUFACTURER, Build.MODEL));
head.put("Android Version", String.format("%s (%d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
head.put("App Version", String.format("%s (%d)", versionName, versionCode));
head.put("Kernel", getKernel());
head.put("Support Abis", Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS != null ? Arrays.toString(Build.SUPPORTED_ABIS): "unknown");
head.put("Fingerprint", Build.FINGERPRINT);
StringBuilder builder = new StringBuilder();
for (String key : head.keySet()) {
if (builder.length() != 0) builder.append("\n");
builder.append(key);
builder.append(" : ");
builder.append(head.get(key));
}
builder.append("\n\n");
builder.append(Log.getStackTraceString(throwable));
return builder.toString();
}
private void writeLog(String log) {
String time = DATE_FORMAT.format(new Date());
File file = new File(mCrashDir, "crash_" + time + ".txt");
try {
write(file, log.getBytes("UTF-8"));
} catch (Throwable e) {
e.printStackTrace();
}
}
private static String getKernel() {
try {
return App.toString(new FileInputStream("/proc/version")).trim();
} catch (Throwable e) {
return e.getMessage();
}
}
}
}
public static final class CrashActivity extends Activity {
private String mLog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTheme(android.R.style.Theme_DeviceDefault);
setTitle("App Crash");
mLog = getIntent().getStringExtra(Intent.EXTRA_TEXT);
ScrollView contentView = new ScrollView(this);
contentView.setFillViewport(true);
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(this);
TextView textView = new TextView(this);
int padding = dp2px(16);
textView.setPadding(padding, padding, padding, padding);
textView.setText(mLog);
textView.setTextIsSelectable(true);
textView.setTypeface(Typeface.DEFAULT);
textView.setLinksClickable(true);
horizontalScrollView.addView(textView);
contentView.addView(horizontalScrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
setContentView(contentView);
}
private void restart() {
Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
finish();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
private static int dp2px(float dpValue) {
final float scale = Resources.getSystem().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(0, android.R.id.copy, 0, android.R.string.copy)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.copy:
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
restart();
}
}
}

View File

@@ -0,0 +1,180 @@
package cc.winboll.studio.autonfc;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.nfc.NfcAdapter;
import android.os.Bundle;
import android.os.IBinder;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.autonfc.nfc.ActionDialog;
import cc.winboll.studio.autonfc.nfc.AutoNFCService;
import cc.winboll.studio.autonfc.nfc.NFCInterfaceActivity;
import cc.winboll.studio.libappbase.LogActivity;
import cc.winboll.studio.libappbase.LogUtils;
public class MainActivity extends AppCompatActivity {
public static final String TAG = "MainActivity";
private NfcAdapter mNfcAdapter;
private PendingIntent mPendingIntent;
private AutoNFCService mService;
private boolean mBound = false;
// 服务连接
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
AutoNFCService.LocalBinder binder = (AutoNFCService.LocalBinder) service;
mService = binder.getService();
mBound = true;
LogUtils.d(TAG, "onServiceConnected: 服务已绑定");
// 关键:把 Activity 传给 Service用于回调
mService.attachActivity(MainActivity.this);
}
@Override
public void onServiceDisconnected(ComponentName name) {
mBound = false;
mService = null;
LogUtils.d(TAG, "onServiceDisconnected: 服务已断开");
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// 初始化 NFC
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
Intent nfcIntent = new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
mPendingIntent = PendingIntent.getActivity(this, 0, nfcIntent, 0);
LogUtils.d(TAG, "onCreate() -> NFC 监听已绑定到 MainActivity");
}
@Override
protected void onStart() {
super.onStart();
// 绑定服务
Intent intent = new Intent(this, AutoNFCService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
LogUtils.d(TAG, "onStart: 绑定服务");
}
@Override
protected void onStop() {
super.onStop();
// 解绑服务
if (mBound) {
unbindService(mConnection);
mBound = false;
LogUtils.d(TAG, "onStop: 解绑服务");
}
}
@Override
protected void onResume() {
super.onResume();
LogUtils.d(TAG, "onResume() -> 开启 NFC 前台分发");
if (mNfcAdapter != null) {
mNfcAdapter.enableForegroundDispatch(this, mPendingIntent, null, null);
}
}
@Override
protected void onPause() {
super.onPause();
LogUtils.d(TAG, "onPause() -> 关闭 NFC 前台分发");
if (mNfcAdapter != null) {
mNfcAdapter.disableForegroundDispatch(this);
}
}
// NFC 卡片靠近唯一入口
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
LogUtils.d(TAG, "onNewIntent() -> 检测到 NFC 卡片");
// 把 NFC 事件交给 Service 处理
if (mBound && mService != null) {
mService.handleNfcIntent(intent);
} else {
LogUtils.e(TAG, "服务未绑定,无法处理 NFC");
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.menu_log) {
LogActivity.startLogActivity(this);
return true;
}
return super.onOptionsItemSelected(item);
}
public void onNFCInterfaceActivity(View view) {
startActivity(new Intent(this, NFCInterfaceActivity.class));
}
// ========================= 【新增】关键方法:由 Service 回调来弹出对话框 =========================
/**
* Service 解析完 NFC 数据后,回调此方法在 Activity 中弹出对话框
*/
public void showNfcActionDialog(final String nfcData) {
LogUtils.d(TAG, "showNfcActionDialog() -> Activity 存活,安全弹出对话框");
// Activity 正在运行,直接弹框,绝对不会报 BadTokenException
final ActionDialog dialog = new ActionDialog(this);
dialog.setNfcData(nfcData);
dialog.setButtonClickListener(new ActionDialog.OnButtonClickListener() {
@Override
public void onBuildClick() {
LogUtils.d(TAG, "点击 Build");
if (mService != null) {
mService.executeTermuxCommand(AutoNFCService.ACTION_BUILD, nfcData);
}
dialog.dismiss();
}
@Override
public void onViewClick() {
LogUtils.d(TAG, "点击 View");
if (mService != null) {
mService.executeTermuxCommand(AutoNFCService.ACTION_BUILD_VIEW, nfcData);
}
dialog.dismiss();
}
@Override
public void onCancelClick() {
dialog.dismiss();
}
});
dialog.show();
}
}

View File

@@ -0,0 +1,66 @@
package cc.winboll.studio.autonfc.models;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/03/16 09:38
*/
public class NfcTermuxCmd {
private String script; // 要执行的预制脚本名(如 auth.sh
private String[] args; // 脚本参数
private String workDir; // 工作目录
private boolean background; // 是否后台执行
private String resultDir; // 结果输出目录(可为 null
public NfcTermuxCmd() {
}
public NfcTermuxCmd(String script, String[] args, String workDir, boolean background, String resultDir) {
this.script = script;
this.args = args;
this.workDir = workDir;
this.background = background;
this.resultDir = resultDir;
}
public String getScript() {
return script;
}
public void setScript(String script) {
this.script = script;
}
public String[] getArgs() {
return args;
}
public void setArgs(String[] args) {
this.args = args;
}
public String getWorkDir() {
return workDir;
}
public void setWorkDir(String workDir) {
this.workDir = workDir;
}
public boolean isBackground() {
return background;
}
public void setBackground(boolean background) {
this.background = background;
}
public String getResultDir() {
return resultDir;
}
public void setResultDir(String resultDir) {
this.resultDir = resultDir;
}
}

View File

@@ -0,0 +1,123 @@
package cc.winboll.studio.autonfc.nfc;
import android.app.Dialog;
import android.content.Context;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import cc.winboll.studio.autonfc.R;
import cc.winboll.studio.libappbase.LogUtils;
/**
* 自定义对话框类,用于与用户交互,展示 NFC 相关操作选项
* 兼容 Java 7 语法
*
* @author 豆包&ZhanGSKen
* @create 2025-08-15
* @lastModify 2026-03-17
*/
public class ActionDialog extends Dialog {
private static final String TAG = "ActionDialog";
private String mNfcData;
private OnButtonClickListener mClickListener;
/**
* 构造函数
*/
public ActionDialog(Context context) {
super(context);
initDialog();
}
/**
* 设置 NFC 数据
*/
public void setNfcData(String nfcData) {
this.mNfcData = nfcData;
LogUtils.d(TAG, "setNfcData() -> " + nfcData);
}
/**
* 设置点击监听
*/
public void setButtonClickListener(OnButtonClickListener listener) {
this.mClickListener = listener;
}
/**
* 初始化布局
*/
private void initDialog() {
setTitle("请选择操作");
LinearLayout layout = new LinearLayout(getContext());
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(20, 20, 20, 20);
addButtons(layout);
setContentView(layout);
}
/**
* 添加按钮
*/
private void addButtons(LinearLayout layout) {
// Build 按钮
Button btnBuild = createButton("Build", new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "点击 Build");
if (mClickListener != null) {
mClickListener.onBuildClick();
}
}
});
layout.addView(btnBuild);
// View 按钮
Button btnView = createButton("View", new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "点击 View");
if (mClickListener != null) {
mClickListener.onViewClick();
}
}
});
layout.addView(btnView);
// 取消按钮
Button btnCancel = createButton("Cancel", new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "点击 Cancel");
dismiss();
}
});
layout.addView(btnCancel);
}
/**
* 创建按钮
*/
private Button createButton(String text, View.OnClickListener listener) {
Button button = new Button(getContext());
button.setText(text);
button.setPadding(10, 10, 10, 10);
button.setOnClickListener(listener);
return button;
}
/**
* 回调接口
*/
public interface OnButtonClickListener {
void onBuildClick();
void onViewClick();
void onCancelClick();
}
}

View File

@@ -0,0 +1,202 @@
package cc.winboll.studio.autonfc.nfc;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.Ndef;
import android.os.Binder;
import android.os.IBinder;
import cc.winboll.studio.autonfc.MainActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import java.nio.charset.Charset;
import java.util.Arrays;
public class AutoNFCService extends Service {
public static final String TAG = "AutoNFCService";
// ================= 已修改:更新为 Beta 包名 =================
public static final String ACTION_BUILD = "cc.winboll.studio.winboll.termux.NfcTermuxBridgeActivity.ACTION_BUILD";
public static final String ACTION_BUILD_VIEW = "cc.winboll.studio.winboll.termux.NfcTermuxBridgeActivity.ACTION_BUILD_VIEW";
private final IBinder mBinder = new LocalBinder();
private String mNfcData;
private MainActivity mActivity; // 持有 Activity 引用,用于回调
// ========================= 生命周期 =========================
@Override
public void onCreate() {
super.onCreate();
LogUtils.d(TAG, "onCreate() -> 服务创建");
// 移除startForeground(NOTIFICATION_ID, buildNotification());
}
@Override
public void onDestroy() {
super.onDestroy();
LogUtils.d(TAG, "onDestroy() -> 服务已停止");
mActivity = null; // 释放引用
}
// ========================= 服务绑定 =========================
@Override
public IBinder onBind(Intent intent) {
LogUtils.d(TAG, "onBind() -> 服务被绑定");
return mBinder;
}
@Override
public boolean onUnbind(Intent intent) {
LogUtils.d(TAG, "onUnbind() -> 服务解绑");
// 移除stopForeground(true);
stopSelf();
return super.onUnbind(intent);
}
// ========================= 对外暴露方法 =========================
/**
* 绑定 Activity用于回调显示对话框
*/
public void attachActivity(MainActivity activity) {
this.mActivity = activity;
}
/**
* 处理 NFC 意图
*/
public void handleNfcIntent(Intent intent) {
LogUtils.d(TAG, "handleNfcIntent() -> 开始处理");
if (intent == null) {
LogUtils.e(TAG, "handleNfcIntent() -> 参数 intent 为空");
return;
}
String action = intent.getAction();
LogUtils.d(TAG, "handleNfcIntent() -> Action = " + action);
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)
|| NfcAdapter.ACTION_TECH_DISCOVERED.equals(action)
|| NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)) {
LogUtils.d(TAG, "handleNfcIntent() -> 匹配 NFC 动作");
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
if (tag == null) {
LogUtils.e(TAG, "handleNfcIntent() -> Tag 为空");
return;
}
LogUtils.d(TAG, "handleNfcIntent() -> Tag ID = " + bytesToHexString(tag.getId()));
LogUtils.d(TAG, "handleNfcIntent() -> Tech List = " + Arrays.toString(tag.getTechList()));
parseNdefData(tag);
}
}
// ========================= 内部业务 =========================
private void parseNdefData(Tag tag) {
LogUtils.d(TAG, "parseNdefData() -> 开始解析");
if (tag == null) return;
Ndef ndef = Ndef.get(tag);
if (ndef == null) {
LogUtils.e(TAG, "parseNdefData() -> 不支持 NDEF 格式");
return;
}
try {
ndef.connect();
NdefMessage msg = ndef.getNdefMessage();
if (msg == null || msg.getRecords() == null || msg.getRecords().length == 0) {
LogUtils.w(TAG, "parseNdefData() -> 卡片无数据");
return;
}
NdefRecord record = msg.getRecords()[0];
byte[] payload = record.getPayload();
int langLen = payload[0] & 0x3F;
int start = 1 + langLen;
if (start < payload.length) {
mNfcData = new String(payload, start, payload.length - start, Charset.forName("UTF-8"));
LogUtils.d(TAG, "parseNdefData() -> 读卡成功: " + mNfcData);
// 关键:回调给 Activity 弹框,此时 Activity 一定是存活状态
if (mActivity != null) {
mActivity.showNfcActionDialog(mNfcData);
}
}
} catch (Exception e) {
LogUtils.e(TAG, "parseNdefData() -> 读取失败", e);
} finally {
try {
ndef.close();
} catch (Exception e) {
// 忽略关闭异常
}
}
}
/**
* 执行 Termux 命令
*/
public void executeTermuxCommand(String action, String nfcData) {
LogUtils.d(TAG, "executeTermuxCommand() -> 开始执行");
if (nfcData == null || nfcData.isEmpty()) {
ToastUtils.show("数据错误");
return;
}
try {
LogUtils.d(TAG, "executeTermuxCommand() -> 发送指令: " + nfcData);
Intent bridgeIntent = new Intent(action);
// ================= 已修改:使用 Beta 包名 =================
bridgeIntent.setClassName(
"cc.winboll.studio.winboll.beta",
"cc.winboll.studio.winboll.termux.NfcTermuxBridgeActivity"
);
bridgeIntent.putExtra(Intent.EXTRA_TEXT, nfcData);
bridgeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(bridgeIntent);
ToastUtils.show("指令已发送");
} catch (Exception e) {
LogUtils.e(TAG, "executeTermuxCommand() -> 发送失败", e);
ToastUtils.show("发送失败");
}
}
// ========================= 工具方法 =========================
private String bytesToHexString(byte[] bytes) {
if (bytes == null || bytes.length == 0) return "";
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X", b));
}
return sb.toString();
}
// ========================= Binder =========================
public class LocalBinder extends Binder {
public AutoNFCService getService() {
return AutoNFCService.this;
}
}
}

View File

@@ -0,0 +1,230 @@
package cc.winboll.studio.autonfc.nfc;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import cc.winboll.studio.autonfc.R;
import cc.winboll.studio.autonfc.models.NfcTermuxCmd;
import cc.winboll.studio.libappbase.LogUtils;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class NFCInterfaceActivity extends Activity {
public static final String TAG = "NFCInterfaceActivity";
private EditText et_script;
private EditText et_args;
private EditText et_workDir;
private EditText et_background;
private EditText et_resultDir;
private TextView tvResult;
private TextView tvStatus;
private NfcAdapter mNfcAdapter;
private PendingIntent mNfcPendingIntent;
private Tag mCurrentTag;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_nfc_interface);
initView();
initNfc();
}
private void initView() {
et_script = findViewById(R.id.et_script);
et_args = findViewById(R.id.et_args);
et_workDir = findViewById(R.id.et_workDir);
et_background = findViewById(R.id.et_background);
et_resultDir = findViewById(R.id.et_resultDir);
tvResult = findViewById(R.id.tv_result);
tvStatus = findViewById(R.id.tv_status);
}
private void initNfc() {
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
if (mNfcAdapter == null) {
tvStatus.setText("设备不支持NFC");
return;
}
if (!mNfcAdapter.isEnabled()) {
tvStatus.setText("请开启NFC");
return;
}
Intent nfcIntent = new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
mNfcPendingIntent = PendingIntent.getActivity(this, 0, nfcIntent, PendingIntent.FLAG_UPDATE_CURRENT);
tvStatus.setText("NFC已启动等待卡片靠近");
}
@Override
protected void onResume() {
super.onResume();
if (mNfcAdapter != null && mNfcAdapter.isEnabled()) {
mNfcAdapter.enableForegroundDispatch(this, mNfcPendingIntent, null, null);
}
}
@Override
protected void onPause() {
super.onPause();
if (mNfcAdapter != null) {
mNfcAdapter.disableForegroundDispatch(this);
}
mCurrentTag = null;
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
mCurrentTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
if (mCurrentTag == null) return;
tvStatus.setText("卡片已连接,解析中...");
readNfc();
}
// -------------------------------------------------------------------------
// 读取 NFC完全委托给工具类
// -------------------------------------------------------------------------
private void readNfc() {
try {
NfcTermuxCmd cmd = NfcUtils.readTag(mCurrentTag);
if (cmd == null) {
tvStatus.setText("读取成功:标签为空");
tvResult.setText("");
// 清空窗体
clearUiFields();
return;
}
// 核心改动:读取成功后,同时更新详情显示 和 窗体输入框
updateUiWithCmd(cmd);
} catch (Exception e) {
LogUtils.e(TAG, "readNfc 失败", e);
tvStatus.setText("读取失败:" + e.getMessage());
// 出错时清空窗体
clearUiFields();
}
}
// -------------------------------------------------------------------------
// 新增:根据读取到的 Cmd 填充 UI详情 + 窗体)
// -------------------------------------------------------------------------
private void updateUiWithCmd(NfcTermuxCmd cmd) {
if (cmd == null) return;
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA).format(new Date());
String show = "【读取时间】 " + time + "\n\n"
+ "【解析结果】\n"
+ "script: " + cmd.getScript() + "\n"
+ "args: " + (cmd.getArgs() != null ? String.join(", ", cmd.getArgs()) : "[]") + "\n"
+ "workDir: " + cmd.getWorkDir() + "\n"
+ "background: " + cmd.isBackground() + "\n"
+ "resultDir: " + cmd.getResultDir();
tvResult.setText(show);
tvStatus.setText("读取成功!");
// 👇 关键逻辑:自动填入窗体(每次读取后都会覆盖输入框)
et_script.setText(cmd.getScript() != null ? cmd.getScript() : "");
et_args.setText(cmd.getArgs() != null ? String.join(",", cmd.getArgs()) : "");
et_workDir.setText(cmd.getWorkDir() != null ? cmd.getWorkDir() : "");
et_background.setText(String.valueOf(cmd.isBackground()));
et_resultDir.setText(cmd.getResultDir() != null ? cmd.getResultDir() : "");
}
// -------------------------------------------------------------------------
// 辅助:清空所有输入框
// -------------------------------------------------------------------------
private void clearUiFields() {
et_script.setText("");
et_args.setText("");
et_workDir.setText("");
et_background.setText("");
et_resultDir.setText("");
}
// -------------------------------------------------------------------------
// 写入按钮(委托给工具类)
// -------------------------------------------------------------------------
public void onWriteClick(View view) {
if (mCurrentTag == null) {
showToast("请先靠近卡片");
return;
}
try {
NfcTermuxCmd cmd = buildCmdFromUI();
NfcUtils.writeTag(mCurrentTag, cmd);
tvStatus.setText("写入成功!");
showToast("写入成功");
readNfc(); // 写入后重读,此时会自动填入窗体
} catch (Exception e) {
LogUtils.e(TAG, "写入失败", e);
tvStatus.setText("写入失败:" + e.getMessage());
showToast("写入失败");
}
}
// -------------------------------------------------------------------------
// 填充调试数据
// -------------------------------------------------------------------------
public void onFillTestDataClick(View view) {
String testJson = "{\"script\":\"BuildWinBoLLProject.sh\",\"args\":[\"DebugTemp\"],\"workDir\":null,\"background\":true,\"resultDir\":null}";
try {
NfcTermuxCmd cmd = NfcUtils.jsonToCmd(testJson);
et_script.setText(cmd.getScript());
et_args.setText(cmd.getArgs() != null ? String.join(",", cmd.getArgs()) : "");
et_workDir.setText(cmd.getWorkDir() != null ? cmd.getWorkDir() : "");
et_background.setText(String.valueOf(cmd.isBackground()));
et_resultDir.setText(cmd.getResultDir() != null ? cmd.getResultDir() : "");
showToast("调试数据已填入");
} catch (Exception e) {
showToast("解析失败");
}
}
// -------------------------------------------------------------------------
// 从 UI 构建 NfcTermuxCmd
// -------------------------------------------------------------------------
private NfcTermuxCmd buildCmdFromUI() {
String script = et_script.getText().toString().trim();
String argsStr = et_args.getText().toString().trim();
String workDir = et_workDir.getText().toString().trim();
String bgStr = et_background.getText().toString().trim();
String resultDir = et_resultDir.getText().toString().trim();
NfcTermuxCmd cmd = new NfcTermuxCmd();
cmd.setScript(script);
cmd.setArgs(argsStr.isEmpty() ? new String[0] : argsStr.split(","));
cmd.setWorkDir(workDir.isEmpty() ? null : workDir);
cmd.setBackground("true".equalsIgnoreCase(bgStr));
cmd.setResultDir(resultDir.isEmpty() ? null : resultDir);
return cmd;
}
private void showToast(String msg) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
}
}

Some files were not shown because too many files have changed in this diff Show More