commit e01f48c9cc1b19497d4bc69cabc0dc02641cadf8 Author: ZhanGSKen Date: Mon Jan 12 13:03:03 2026 +0800 Clone https://gitea.winboll.cc/Studio/WinBoLL_Bck20260112_122031_590.git WinBoLL 项目基础框架。 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..110c6f0 --- /dev/null +++ b/.gitignore @@ -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 + +## 忽略模块应用编译配置 +/settings.gradle +/gradle.properties +/winboll.properties +/local.properties diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c97416e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "libjc/jcc/libs"] + path = libjc/jcc/libs + url = https://gitea.winboll.cc/Studio/APP_libjc_jcc_libs.git diff --git a/GenKeyStore/gen_debug_keystore.sh b/GenKeyStore/gen_debug_keystore.sh new file mode 100644 index 0000000..6ecd6ac --- /dev/null +++ b/GenKeyStore/gen_debug_keystore.sh @@ -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 < ${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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1eac983 --- /dev/null +++ b/README.md @@ -0,0 +1,165 @@ +# OriginMaster +【OriginMaster】WinBoLL 源生态计划。正如话,我需要一个 Point, 去撬动成个地球。 + +######## +## ☁ ☁ ☁ WinBoLL APP ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ +# ☁ ☁ WinBoLL Studio Android 应用开源项目。☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ +# ☁ ☁ ☁ WinBoLL 网站地址 https://www.winboll.cc/ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ +# ☁ ☁ ☁ WinBoLL 源码地址 ☁ ☁ ☁ ☁ ☁ ☁ ☁ +# ☁ ☁ ☁ GitHub 源码地址 ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ +# ☁ ☁ ☁ 码云 源码地址 ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ +# ☁ ☁ ☁ 在 jitpack.io 托管的 APPBase 类库源码 ☁ ☁ ☁ ☁ +# ☁ ☁ ☁ 在 jitpack.io 托管的 AES 类库源码 ☁ ☁ ☁ ☁ +## WinBoLL 提问 +同样是 /sdcard 目录,在开发 Android 应用时, +能否实现手机编译与电脑编译的源码同步。 +☁因而 WinBoLL 项目组诞生了。 + +## WinBoLL 项目组研发计划 +致力于把 WinBoLL-APP 应用在手机端 Android 项目开发。 +也在探索 https://gitea.winboll.cc//APP.git 应用于 WinBoLL-APP APK 分发。 +更想进阶 https://github.com//APP.git 应用于 WinBoLL-APP Beta APK 分发。 + +## WinBoLL-APP 汗下... +#### ☁应用何置如此呢。且观用户云云。 + +#### ☁ 正当下 ☁ ### +#### ☁ 且容傻家叙说 ☁ WinBoLL-APP 应用场景 +### ☁ WinBoLL 设备资源概述 +#### ☁ 1. Raid Disk. +概述:这是一个矩阵存储类设备。 +优点:该设备具有数据容错存储功能, + 数据存储具有特长持久性。 +缺点:设备使用能源消耗比较高, + 设备存取速度一般。 + +#### ☁ 2. Data Disk. +概述:这是一个普通硬盘存储设备 +优点:该设备独立于操作系统, + 数据持久性一般, + 存取能源消耗小于 Raid Disk。 +缺点:数据存储速度一般,存储能源消耗一般。 + +#### ☁ 3. SSD Disk. +概述:这是一个 SSD 硬盘存储设备。 +优点:存取速度快于 Data Disk 与 Raid Disk, + 存取能源消耗小于 Data Disk 与 Raid Disk。 +缺点:数据持久性一般, + 设备位于操作系统内部文件系统。 + 数据持久性与操作系统挂钩。 + +#### ☁ 4. WinBoLL 用户资源概述。 +1> /home/<用户名> 位于 WinBoLL 操作系统目录下。 +2> /rdisk/<用户名> 挂载用户 Raid Disk. +3> /data/<用户名> 挂载用户 Data Disk. +4> /sdcard/<用户名> 挂载用户 SSD Disk. + +#### ☁ 5. WinBoLL-APP 用户资源概述。 +1> /sdcard 挂载用户手机 SD 存储/storage/emulated/0 + +### ☁ 稍稍歇 ☁ ### +### ☁ 急急停 ☁ WinBoLL 应用前置条件 +☁ WinBoLL 主机建立 1Panel MySQL 应用。 +☁ WinBoLL 主机建立 1Panel Gitea 应用。 +☁ WinBoLL 主机设置 WinBoLL 应用为非登录状态。 +☁ WinBoLL 主机建立 WinBoLL 账户与 WinBoLL 用户组。 +☁ WinBoLL 账户 User ID 为: J。 +☁ WinBoLL 用户组 Group ID 为: Studio。 +☁ WinBoLL 主机 WinBoLL 1Panel Gitea 建立 WinBoLL 工作组。 +☁ WinBoLL 主机 WinBoLL 1Panel Gitea 用户项目 APK 编译输出目录为 /sdcard/WinBoLLStudio/<用户名>/APKs/ +☁ WinBoLL 项目配置文件示例为 "/.winboll/winboll.properties-demo"(WinBoLL 项目已设置) +☁ WinBoLL 项目配置文件为 "/.winboll/winboll.properties" +☁ WinBoLL 项目配置文件设定为源码提交时忽略。(WinBoLL 项目已设置) +☁ Gradle 项目配置文件示例为 "/.winboll/local.properties-demo"(WinBoLL 项目已设置) +☁ Gradle 项目配置文件为 "/local.properties"(WinBoLL 项目已设置) +☁ Gradle 项目配置文件设定为源码提交时忽略。(WinBoLL 项目已设置) + +### ☁ 登高处 ☁ WinBoLL 应用需求规划 +☁ WinBoLL 主机建立 WinBoLL 客户端用户数据库为 MySQL winbollclient 数据库。 +☁ WinBoLL 主机设置 WinBoLL 客户端用户信息存储在 winbollclient 数据库中。 +☁ MySQL winbollclient 数据库中 + WinBoLL 客户端用户信息设定为: + <用户名, 验证密码, 验证邮箱, 验证手机, 唯一存储令牌Token, 备用验证邮箱>。 +☁ WinBoLL 项目源码仓库托管在 WinBoLL 1Panel Gitea 目录 /opt/1panel/apps/gitea/gitea/data/git/repositories/studio/app.git中。 +☁ WinBoLL 主机提供 WinBoLL 1Panel Gitea 应用的 WinBoLL 项目源码仓库存取功能。(Gitea 应用已提供) +☁ WinBoLL 主机提供 WinBoLL Gitea 项目仓库存档功能。(Gitea 应用已提供) +☁ 提供 WinBoLL 客户端用户登录功能。(Gitea 应用已提供) + +### ☁ 看远方 ☁ ### +### ☁ 心忧虑 ☁ WinBoLL-APP 应用前置需求 +☁ WinBoLL-APP WinBoLL 项目根目录设定为手机的 /sdcard/WinBoLLStudio/Sources 目录。(需要用户手动建立文件夹) +☁ WinBoLL-APP 具有手机 /sdcard/WinBoLL 目录的存储权限。(需要手机操作系统授权) +☁ WinBoLL-APP WinBoLL 项目仓库源码存储路径为 /sdcard/WinBoLLStudio/Sources/APP.git(需要用户手动建立文件夹) +☁ WinBoLL-APP 项目 APK 编译输出目录为 /sdcard/WinBoLLStudio/APKs/ +☁ WinBoLL-APP 应用签名验证可定制化。(WinBoLL 项目已提供) +☁ WinBoLL-APP 与系列衍生 APP 应用共享 cc.winboll.studio 命名空间资源。(WinBoLL 项目已提供) +☁ WinBoLL-APP 用户客户端信息存储在命名空间为 WinBoLL APP MySQLLite 应用的 winbollappclient 数据库中。 +☁ WinBoLL-APP MySQLLite 应用的 winbollappclient 数据库中, + WinBoLL 用户客户端信息设定为: + <用户名, 唯一存储令牌Token>。 + +### ☁ 云游四方 ☁ ### +### ☁ 呔! ☁ WinBoLL-APP 应用需求规划 +☁ 如要使用 WinBoLL Android 项目的 Gradle 编译功能,则需要设置以下两个文件夹。 +☁ 1. 则需要建立数据存储目录 /sdcard/WinBoLLStudio/APKs。 + WinBoLL 项目源码编译出来的安装包会拷贝一份到 /sdcard/WinBoLLStudio/APKs 目录下。 +☁ 2. 则需要建立数据存储目录 /sdcard/AppProjects。 + WinBoLL 项目源码编译出来的安装包会拷贝一份并命名 "app.apk" 的安装文件为到 /sdcard/AppProjects 目录下。 + + +### ☁ 吁! ☁ WinBoLL-APP 共享计划前景 +☁ WinBoLL-APP 将会实现 https://winboll.cc/api 访问功能。 +☁ WinBoLL-APP 将会实现手机端 Android 应用的开发与管理功能。 + +## ☁ WinBoLL ☁ WinBoLL 主机忧虑 +☁ WinBoLL 将会提供 gitea.winboll.cc 域名用户注册登录功能。 +☁ WinBoLL 将会提供 WinBoLL-APP 及其衍生应用的 Gitea 仓库管理服务。 +☁ WinBoLL 将会提供 winboll.cc 域名 WinBoLL 项目组注册登录功能。 + +# 本项目要实际运用需要注意以下几个步骤: +# 在项目根目录下: +## ★. 项目模块编译环境设置(必须),settings.gradle-demo 要复制为 settings.gradle,并取消相应项目模块的注释。 +## ★. 项目模块编译环境设置(必须) 在根目录拷贝 gradle.properties-androidx-demo 或者 gradle.properties-android-demo 文件为 gradle.properties。 +## ★. 项目 Android SDK 编译环境设置(可选),local.properties-demo 要复制为 local.properties,并按需要设置 Android SDK 目录。 +## ★. 应用签名密钥 keystore 设置问题。一般调试编译只需用【Termux】cd 进 GenKeyStore 目录执行 $ bash gen_debug_keystore.sh 命令即可完成设置。 +## ☆. 应用 WiBoLL 签名密钥配置问题<非必须考虑>。设置时需要 clone 【keystore】模块源码并拷贝模块目录的 appkey.jks 与 appkey.keystore 到项目根目录即可。 +## ☆. 类库型模块编译环境设置(可选),winboll.properties-demo 要复制为 winboll.properties,并按需要设置 WinBoLL Maven 库登录用户信息, 和 APK 文件额外输出路径。 + + +# ☆类库型项目编译方法 +## 先编译类库对应的模块测试项目 +### 修改模块测试项目的 build.properties 文件 +设置属性 libraryProject=<类库项目模块文件夹名称> +### 再编译测试项目 +$ bash .winboll/bashPublishAPKAddTag.sh <应用项目模块文件夹名称> +#### 测试项目编译后,编译器会复制一份 APK 到 路径:"/sdcard/WinBoLLStudio/APKs/<项目根目录名称>/tag/" 文件夹。 +#### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。 +### 最后编译类库项目 +$ bash .winboll/bashPublishLIBAddTag.sh <类库项目模块文件夹名称> +#### 类库模块编译命令执行后,编译器会发布到 WinBoLL Nexus Maven 库:Maven 库地址可以参阅根项目目录配置 build.gradle 文件。 + +# ☆应用型项目编译方法 +## 直接调用以下命令编译应用型项目 +$ bash .winboll/bashPublishAPKAddTag.sh <应用项目模块文件夹名称> +#### 应用模块编译命令执行后,编译器会复制一份 APK 到 +#### 测试项目编译后,编译器会复制一份 APK 到 路径:"/sdcard/WinBoLLStudio/APKs/<项目根目录名称>/tag/" 文件夹。 +#### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。 + +## ☆应用调试编译方法 +使用以下命令编译调试: + +### Beta调试使用: +$ bash gradlew assembleBetaDebug + +### Stage调试使用: +$ bash gradlew assembleStageDebug + +### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。 + +# 应用版本号命名方式 +## statge 渠道 +V<应用开发环境编号><应用功能变更号><应用调试阶段号> +如:APPBase_15.7.0 +## beta 渠道 +V<应用开发环境编号><应用功能变更号><应用调试阶段号>-beta<调试编译计数>_<调试编译时间(分钟+秒钟)> +如:APPBase_15.9.6-beta8_5413 diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..55d02df --- /dev/null +++ b/build.gradle @@ -0,0 +1,117 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + mavenLocal { + // 设置本地Maven仓库路径 + url 'file:///sdcard/.m2/repository/' + } + + // Nexus Maven 库地址 + // "WinBoLL Release" + maven { url "https://nexus.winboll.cc/repository/maven-public/" } + // "WinBoLL Snapshot" + maven { url "https://nexus.winboll.cc/repository/maven-snapshots/" } + + maven { url 'https://maven.aliyun.com/repository/public/' } + maven { url 'https://maven.aliyun.com/repository/google/' } + maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' } + maven { url 'https://dl.bintray.com/ppartisan/maven/' } + maven { url "https://clojars.org/repo/" } + maven { url "https://jitpack.io" } + mavenCentral() + google() + //println "mavenLocal : ===========" + //println mavenLocal().url + //println "mavenLocal : ===========" + //mavenLocal() + } + dependencies { + // 适配MIUI12 + classpath 'com.android.tools.build:gradle:7.2.1' // 对应 compileSdkVersion 32 + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + mavenLocal { + // 设置本地Maven仓库路径 + url 'file:///sdcard/.m2/repository/' + } + + // Nexus Maven 库地址 + // "WinBoLL Release" + maven { url "https://nexus.winboll.cc/repository/maven-public/" } + // "WinBoLL Snapshot" + maven { url "https://nexus.winboll.cc/repository/maven-snapshots/" } + + maven { url 'https://maven.aliyun.com/repository/public/' } + maven { url 'https://maven.aliyun.com/repository/google/' } + maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' } + maven { url 'https://dl.bintray.com/ppartisan/maven/' } + maven { url "https://clojars.org/repo/" } + maven { url "https://jitpack.io" } + mavenCentral() + google() + //println "mavenLocal : ===========" + //println mavenLocal().url + //println "mavenLocal : ===========" + //mavenLocal() + } + ext { + // 定义全局变量,常用于版本管理 + // 变量在子模块的build.gradle中直接以: $NAME 的形式调用 + // 定义版本管理文件 + RootProjectDir = "${rootProject.projectDir}".replace("\\", "/") + bashCheckGitCommitStatusFilePath = ".winboll/bashCheckGitCommitStatus.sh" + bashCommitAppPublishBuildFlagInfoFilePath = ".winboll/bashCommitAppPublishBuildFlagInfo.sh" + + winbollFilePath = "winboll.properties" + keyPropsFilePath = "appkey.keystore" + // 定义 lint 输出文件 + lintXmlReportFilePath = "build/reports/lint-results.xml" + lintHTMLReportFilePath = "build/reports/lint-results.html" + + // 检查编译配置文件 + subProjectPath = "$RootProjectDir/$project.name" + println "Sub project path: $subProjectPath" + winbollBuildPropsDesc="Created by .winboll/winboll_app_build.gradle" + winbollBuildPropsFile = rootProject.file("$subProjectPath/build.properties") + winbollBuildPropsFilePath = winbollBuildPropsFile.getAbsolutePath(); + assert(winbollBuildPropsFile.exists()) + winbollBuildProps = new Properties() + // 读取编译标志位配置文件 + winbollBuildProps.load(new FileInputStream(winbollBuildPropsFile)) + // 读取编译标志位配置文件 + assert (winbollBuildProps['stageCount'] != null) + assert (winbollBuildProps['baseVersion'] != null) + assert (winbollBuildProps['publishVersion'] != null) + assert (winbollBuildProps['buildCount'] != null) + } + + afterEvaluate { + task cleanLintFile() { + dependsOn tasks.findAll { task -> task.name.startsWith('lint') } + doFirst { + // 这里是将在Lint任务开始前执行的代码 + println "Lint task will run after this setup." + } + } + } + + subprojects { + // 1. 对纯 Java 模块的 JavaCompile 任务配置(升级为 Java 11) + tasks.withType(JavaCompile) { + options.compilerArgs << "-parameters" + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + // 可选:确保编码一致 + options.encoding = "UTF-8" + } + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties-android-demo b/gradle.properties-android-demo new file mode 100644 index 0000000..cb39be8 --- /dev/null +++ b/gradle.properties-android-demo @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=false +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=false +# 保持与旧版Gradle插件的兼容 +android.disableAutomaticComponentCreation=true diff --git a/gradle.properties-androidx-demo b/gradle.properties-androidx-demo new file mode 100644 index 0000000..74b1f76 --- /dev/null +++ b/gradle.properties-androidx-demo @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# 保持与旧版Gradle插件的兼容 +android.disableAutomaticComponentCreation=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3124347 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl = https\://services.gradle.org/distributions/gradle-7.5.1-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..744e882 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/local.properties-demo b/local.properties-demo new file mode 100644 index 0000000..1fc1081 --- /dev/null +++ b/local.properties-demo @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Sat Apr 27 01:35:26 CST 2024 +#sdk.dir= diff --git a/settings.gradle-demo b/settings.gradle-demo new file mode 100644 index 0000000..893d3a6 --- /dev/null +++ b/settings.gradle-demo @@ -0,0 +1,79 @@ +// AutoInstaller 项目编译设置 +//include ':autoinstaller' +//rootProject.name = "autoinstaller" + +// MJ 项目编译设置 +//include ':mj' +//rootProject.name = "mj" + +// PowerBell 项目编译设置 +//include ':powerbell' +//rootProject.name = "powerbell" + +// APPBase 项目编译设置 +//include ':appbase' +//include ':libappbase' +//rootProject.name = "appbase" + +// APPUtils 项目编译设置 +//include ':apputils' +//include ':libapputils' +//rootProject.name = "apputils" + +// JC 项目编译设置 +//include ':jc' +//include ':libjc' +//rootProject.name = "jc" + +// AES 项目编译设置 +//include ':aes' +//include ':libaes' +//rootProject.name = "aes" + +// Contacts 项目编译设置 +//include ':contacts' +//rootProject.name = "contacts" + +// MyMessageManager 项目编译设置 +//include ':mymessagemanager' +//rootProject.name = "mymessagemanager" + +// TimeStamp 项目编译设置 +//include ':timestamp' +//rootProject.name = "timestamp" + +// AndroidDemo 项目编译设置 +//include ':androiddemo' +//rootProject.name = "androiddemo" + +// AndroidXDemo 项目编译设置 +//include ':androidxdemo' +//rootProject.name = "androidxdemo" + +// Ollama 项目编译设置 +//include ':ollama' +//rootProject.name = "ollama" + +// NumTable 项目编译设置 +//include ':numtable' +//rootProject.name = "numtable" + +// MidiPlayer 项目编译设置 +//include ':midiplayer' +//rootProject.name = "midiplayer" + +// WebPageSources 项目编译设置 +//include ':webpagesources' +//rootProject.name = "webpagesources" + +// Positions 项目编译设置 +//include ':positions' +//rootProject.name = "positions" + +// WinBoLL 项目编译设置 +//include ':winboll' +//rootProject.name = "winboll" + +// RegExpUtils 项目编译设置 +//include ':regexputils' +//rootProject.name = "regexputils" diff --git a/winboll.properties-demo b/winboll.properties-demo new file mode 100644 index 0000000..6b927a9 --- /dev/null +++ b/winboll.properties-demo @@ -0,0 +1,13 @@ +## WinBoLL Nexus Settings +## These properties is setting for publishing +## library project to WinBoLL Nexus Maven Repository. +## + +## WinBoLL Extra APK file Output Path +#ExtraAPKOutputPath=/sdcard/AppProjects/app.apk + +## WinBoLL Nexus UserName +#Nexus.name=nexustestuser1 + +## WinBoLL Nexus Password +#Nexus.password=nexustestuserpassword diff --git a/winboll/README.md b/winboll/README.md new file mode 100644 index 0000000..2de1c76 --- /dev/null +++ b/winboll/README.md @@ -0,0 +1,34 @@ +# WinBoLL + +#### 介绍 +WinBoLL 网站浏览器。 + +#### 软件架构 +适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。 +也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。 + + +#### Gradle 编译说明 +调试版编译命令 :gradle assembleBetaDebug +阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh winboll + +#### 使用说明 + +#### 参与贡献 + +1. Fork 本仓库 +2. 新建 Feat_xxx 分支 +3. 提交代码 : ZhanGSKen(ZhanGSKen) +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/) + +#### 参考文档 diff --git a/winboll/app_update_description.txt b/winboll/app_update_description.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/winboll/app_update_description.txt @@ -0,0 +1 @@ + diff --git a/winboll/build.gradle b/winboll/build.gradle new file mode 100644 index 0000000..820fc34 --- /dev/null +++ b/winboll/build.gradle @@ -0,0 +1,101 @@ +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.winboll" + 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' + + // WinBoLL库 nexus.winboll.cc 地址 + api 'cc.winboll.studio:libaes:15.12.13' + api 'cc.winboll.studio:libappbase:15.14.2' + + // WinBoLL备用库 jitpack.io 地址 + //api 'com.github.ZhanGSKen:AES:aes-v15.12.9' + //api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1' + + api fileTree(dir: 'libs', include: ['*.jar']) +} diff --git a/winboll/build.properties b/winboll/build.properties new file mode 100644 index 0000000..53f0cc9 --- /dev/null +++ b/winboll/build.properties @@ -0,0 +1,8 @@ +#Created by .winboll/winboll_app_build.gradle +#Sun Jan 11 04:13:06 GMT 2026 +stageCount=9 +libraryProject= +baseVersion=15.11 +publishVersion=15.11.8 +buildCount=14 +baseBetaVersion=15.11.9 diff --git a/winboll/libs/libWeWorkSpecSDK.so b/winboll/libs/libWeWorkSpecSDK.so new file mode 100644 index 0000000..d9f592b Binary files /dev/null and b/winboll/libs/libWeWorkSpecSDK.so differ diff --git a/winboll/proguard-rules.pro b/winboll/proguard-rules.pro new file mode 100644 index 0000000..a18de74 --- /dev/null +++ b/winboll/proguard-rules.pro @@ -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 ; +} + +# 保留 native 方法(避免JNI调用失败) +-keepclasseswithmembernames class * { + native ; +} + +# 保留注解和泛型(避免反射/序列化异常) +-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 { + (); +} +-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 ; +} + +# 米盟 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 + diff --git a/winboll/src/beta/AndroidManifest.xml b/winboll/src/beta/AndroidManifest.xml new file mode 100644 index 0000000..be35225 --- /dev/null +++ b/winboll/src/beta/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/winboll/src/beta/res/values-zh/strings.xml b/winboll/src/beta/res/values-zh/strings.xml new file mode 100644 index 0000000..045e125 --- /dev/null +++ b/winboll/src/beta/res/values-zh/strings.xml @@ -0,0 +1,3 @@ + + + diff --git a/winboll/src/beta/res/values/strings.xml b/winboll/src/beta/res/values/strings.xml new file mode 100644 index 0000000..42cbd7a --- /dev/null +++ b/winboll/src/beta/res/values/strings.xml @@ -0,0 +1,8 @@ + + + + WinBoLL+ + 筋斗云★ + 金抖云☆ + + diff --git a/winboll/src/beta/res/xml/shortcutsmaincn1.xml b/winboll/src/beta/res/xml/shortcutsmaincn1.xml new file mode 100644 index 0000000..6b6dc9a --- /dev/null +++ b/winboll/src/beta/res/xml/shortcutsmaincn1.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + diff --git a/winboll/src/beta/res/xml/shortcutsmaincn2.xml b/winboll/src/beta/res/xml/shortcutsmaincn2.xml new file mode 100644 index 0000000..2dbb833 --- /dev/null +++ b/winboll/src/beta/res/xml/shortcutsmaincn2.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + diff --git a/winboll/src/beta/res/xml/shortcutsmainen1.xml b/winboll/src/beta/res/xml/shortcutsmainen1.xml new file mode 100644 index 0000000..4684c41 --- /dev/null +++ b/winboll/src/beta/res/xml/shortcutsmainen1.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + diff --git a/winboll/src/main/AndroidManifest.xml b/winboll/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8236f00 --- /dev/null +++ b/winboll/src/main/AndroidManifest.xml @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/App.java b/winboll/src/main/java/cc/winboll/studio/winboll/App.java new file mode 100644 index 0000000..9bfaab0 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/App.java @@ -0,0 +1,366 @@ +package cc.winboll.studio.winboll; + +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; +import cc.winboll.studio.libaes.utils.WinBoLLActivityManager; + +public class App extends GlobalApplication { + + public static final String TAG = "App"; + + public static final String COMPONENT_EN1 = "cc.winboll.studio.winboll.MainActivityEN1"; + public static final String COMPONENT_CN1 = "cc.winboll.studio.winboll.MainActivityCN1"; + public static final String COMPONENT_CN2 = "cc.winboll.studio.winboll.MainActivityCN2"; + public static final String ACTION_SWITCHTO_EN1 = "cc.winboll.studio.winboll.App.ACTION_SWITCHTO_EN1"; + public static final String ACTION_SWITCHTO_CN1 = "cc.winboll.studio.winboll.App.ACTION_SWITCHTO_CN1"; + public static final String ACTION_SWITCHTO_CN2 = "cc.winboll.studio.winboll.App.ACTION_SWITCHTO_CN2"; + + private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper()); + + @Override + public void onCreate() { + super.onCreate(); + setIsDebugging(BuildConfig.DEBUG); + //setIsDebugging(false); + + WinBoLLActivityManager.init(this); + + // 初始化 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); + } + + @Override + public void onTerminate() { + super.onTerminate(); + ToastUtils.release(); + } + + + + 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 head = new LinkedHashMap(); + 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(); + } + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/AssistantService.java b/winboll/src/main/java/cc/winboll/studio/winboll/AssistantService.java new file mode 100644 index 0000000..fb23c79 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/AssistantService.java @@ -0,0 +1,97 @@ +package cc.winboll.studio.winboll; + +/** + * @Author ZhanGSKen + * @Date 2025/03/28 19:12:12 + * @Describe 应用主要服务组件类守护进程服务组件类 + */ +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import cc.winboll.studio.libaes.models.WinBoLLClientServiceBean; +import cc.winboll.studio.winboll.WinBoLLClientService; +import cc.winboll.studio.winboll.utils.ServiceUtils; + +public class AssistantService extends Service { + + public final static String TAG = "AssistantService"; + + WinBoLLClientServiceBean mWinBoLLServiceBean; + MyServiceConnection mMyServiceConnection; + volatile boolean mIsServiceRunning; + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + mWinBoLLServiceBean = WinBoLLClientServiceBean.loadWinBoLLClientServiceBean(this); + if (mMyServiceConnection == null) { + mMyServiceConnection = new MyServiceConnection(); + } + // 设置运行参数 + mIsServiceRunning = false; + run(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + run(); + return START_STICKY; + } + + @Override + public void onDestroy() { + mIsServiceRunning = false; + super.onDestroy(); + } + + // + // 运行服务内容 + // + void run() { + mWinBoLLServiceBean = WinBoLLClientServiceBean.loadWinBoLLClientServiceBean(this); + if (mWinBoLLServiceBean.isEnable()) { + if (mIsServiceRunning == false) { + // 设置运行状态 + mIsServiceRunning = true; + // 唤醒和绑定主进程 + wakeupAndBindMain(); + } + } + } + + // + // 唤醒和绑定主进程 + // + void wakeupAndBindMain() { + if (ServiceUtils.isServiceAlive(getApplicationContext(), WinBoLLClientService.class.getName()) == false) { + startForegroundService(new Intent(AssistantService.this, WinBoLLClientService.class)); + } + + bindService(new Intent(AssistantService.this, WinBoLLClientService.class), mMyServiceConnection, Context.BIND_IMPORTANT); + } + + // + // 主进程与守护进程连接时需要用到此类 + // + class MyServiceConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mWinBoLLServiceBean = WinBoLLClientServiceBean.loadWinBoLLClientServiceBean(AssistantService.this); + if (mWinBoLLServiceBean.isEnable()) { + wakeupAndBindMain(); + } + } + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/CustomToolbar.java b/winboll/src/main/java/cc/winboll/studio/winboll/CustomToolbar.java new file mode 100644 index 0000000..d9d255b --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/CustomToolbar.java @@ -0,0 +1,53 @@ +package cc.winboll.studio.winboll; + +/** + * @Author ZhanGSKen + * @Date 2025/05/22 13:08 + */ +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.Toolbar; + +public class CustomToolbar extends Toolbar { + + private View viewMain; + + public CustomToolbar(Context context) { + this(context, null); + } + + public CustomToolbar(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public CustomToolbar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initView(context, attrs); + } + + private void initView(Context context, AttributeSet attrs) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomToolbar); + + // 获取属性值 + String toolbarTitle = typedArray.getString(R.styleable.CustomToolbar_toolbarTitle); + int toolbarTitleColor = typedArray.getColor(R.styleable.CustomToolbar_toolbarTitleColor, android.graphics.Color.WHITE); + int toolbarBackgroundColor = typedArray.getColor(R.styleable.CustomToolbar_toolbarBackgroundColor, android.graphics.Color.BLUE); + + // 加载布局 + viewMain = LayoutInflater.from(context).inflate(R.layout.view_toolbar, this, true); + + // 应用属性值 + TextView toolbarTitleTextView = viewMain.findViewById(R.id.toolbar_title); + toolbarTitleTextView.setText(toolbarTitle); + toolbarTitleTextView.setTextColor(toolbarTitleColor); + viewMain.setBackgroundColor(toolbarBackgroundColor); + + // 释放 TypedArray + typedArray.recycle(); + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/EWUIStatusIconDrawable.java b/winboll/src/main/java/cc/winboll/studio/winboll/EWUIStatusIconDrawable.java new file mode 100644 index 0000000..20a70d8 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/EWUIStatusIconDrawable.java @@ -0,0 +1,35 @@ +package cc.winboll.studio.winboll; + +/** + * @Author ZhanGSKen + * @Date 2025/03/28 19:11:27 + * @Describe WinBoLL UI 状态图标枚举 + */ +import cc.winboll.studio.libaes.R; + +public enum EWUIStatusIconDrawable { + NORMAL(0), + NEWS(1) + ; + + static final String TAG = "WUIStatusIconDrawable"; + + static String[] _mlistCNName = { "正常", "新的消息" }; + + private int value = 0; + private EWUIStatusIconDrawable(int value) { //必须是private的,否则编译错误 + this.value = value; + } + + public static int getIconDrawableId(EWUIStatusIconDrawable drawableId) { + int res; + switch(drawableId){ + case NEWS : + res = R.drawable.ic_winbollbeta; + break; + default : + res = R.drawable.ic_winboll; + } + return res; + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/MainActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/MainActivity.java new file mode 100644 index 0000000..935b86f --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/MainActivity.java @@ -0,0 +1,199 @@ +package cc.winboll.studio.winboll; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import cc.winboll.studio.libaes.activitys.AboutActivity; +import cc.winboll.studio.libaes.activitys.DrawerFragmentActivity; +import cc.winboll.studio.libaes.models.APPInfo; +import cc.winboll.studio.libaes.models.DrawerMenuBean; +import cc.winboll.studio.libaes.utils.WinBoLLActivityManager; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.winboll.R; +import cc.winboll.studio.winboll.activities.SettingsActivity; +import cc.winboll.studio.winboll.activities.WXPayActivity; +import cc.winboll.studio.winboll.fragments.BrowserFragment; +import java.util.ArrayList; + +public class MainActivity extends DrawerFragmentActivity { + + + public static final String TAG = "MainActivity"; + + BrowserFragment mBrowserFragment; + + // ------------------- 新增:Handler 消息定义(接收URL历史更新消息) ------------------- + // 消息标识:URL加载历史更新(刷新抽屉菜单的历史列表) + public static final int MSG_URLLOADHISTORY_UPDATE = 1002; + // 自定义Handler(接收应用内消息,如BrowserFragment发送的历史更新消息) + private static Handler _mMainHandler; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // ------------------- 新增:初始化MainActivity的Handler(关键) ------------------- + initMainHandler(); + + if (mBrowserFragment == null) { + mBrowserFragment = new BrowserFragment(); + addFragment(mBrowserFragment); + } + showFragment(mBrowserFragment); + } + + public static void sendMessage(Message msg) { + _mMainHandler.sendMessage(msg); + } + + /** + * 初始化Handler(接收MSG_URLLOADHISTORY_UPDATE消息,刷新抽屉历史菜单) + */ + private void initMainHandler() { + // 清理旧数据 + if (_mMainHandler != null) { + _mMainHandler.removeCallbacksAndMessages(null); + _mMainHandler = null; + } + + // Java 7 匿名内部类实现Handler(主线程创建,安全更新UI/抽屉菜单) + _mMainHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + super.handleMessage(msg); + switch (msg.what) { + case MSG_URLLOADHISTORY_UPDATE: + // 处理URL历史更新消息:刷新抽屉菜单的历史列表 + LogUtils.d(TAG, "收到URL历史更新消息,刷新抽屉菜单"); + refreshUrlHistoryDrawerMenu(); + break; + default: + break; + } + } + }; + } + + @Override + public void initDrawerMenuItemList(ArrayList listDrawerMenu) { + super.initDrawerMenuItemList(listDrawerMenu); + //LogUtils.d(TAG, "initDrawerMenuItemList"); + // 加载URL历史菜单(初始化时加载) + refreshUrlHistoryDrawerMenu(); + notifyDrawerMenuDataChanged(); + } + + @Override + public void reinitDrawerMenuItemList(ArrayList listDrawerMenu) { + super.reinitDrawerMenuItemList(listDrawerMenu); + //LogUtils.d(TAG, "reinitDrawerMenuItemList"); + // 重新加载URL历史菜单(菜单重置时加载) + refreshUrlHistoryDrawerMenu(); + notifyDrawerMenuDataChanged(); + } + + void loadUrlLoadHistotyMenu(ArrayList listDrawerMenu) { + listDrawerMenu.clear(); + if (BrowserFragment._mUrlLoadHistory != null) { + for (String url : BrowserFragment._mUrlLoadHistory) { + listDrawerMenu.add(new DrawerMenuBean(R.drawable.ic_launcher, url)); + } + } + } + + // ------------------- 新增:刷新URL历史抽屉菜单(提取独立方法,复用) ------------------- + private void refreshUrlHistoryDrawerMenu() { + // 获取抽屉菜单列表,重新加载历史数据并刷新 + ArrayList drawerMenuList = super.malDrawerMenuItem; // 假设父类提供获取菜单列表的方法 + if (drawerMenuList != null) { + loadUrlLoadHistotyMenu(drawerMenuList); // 重新加载更新后的历史数据 + notifyDrawerMenuDataChanged(); // 通知抽屉菜单刷新UI + } + } + + @Override + public DrawerFragmentActivity.ActivityType initActivityType() { + return DrawerFragmentActivity.ActivityType.Main; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.toolbar_main, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + super.onItemClick(parent, view, position, id); + if (mBrowserFragment != null && mBrowserFragment.getBrowserHandler() != null) { + Message msg = Message.obtain(); + msg.what = BrowserFragment.MSG_HISTORY_POSITION; + msg.obj = position; + mBrowserFragment.getBrowserHandler().sendMessage(msg); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int nItemId = item.getItemId(); + if (nItemId == R.id.item_home) { + // 发送MSG_HOMEPAGE消息给BrowserFragment + if (mBrowserFragment != null && mBrowserFragment.getBrowserHandler() != null) { + Message msg = Message.obtain(); + msg.what = BrowserFragment.MSG_HOMEPAGE; + mBrowserFragment.getBrowserHandler().sendMessage(msg); + } + } else if (nItemId == R.id.item_settings) { + WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), SettingsActivity.class); + } else if (nItemId == R.id.item_wxpayactivity) { + WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), WXPayActivity.class); + } else if (nItemId == cc.winboll.studio.libaes.R.id.item_about) { + Intent intent = new Intent(getApplicationContext(), AboutActivity.class); + APPInfo appInfo = genDefaultAPPInfo(); + intent.putExtra(AboutActivity.EXTRA_APPINFO, appInfo); + WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), intent, AboutActivity.class); + } else { + return super.onOptionsItemSelected(item); + } + + return true; + } + + + APPInfo genDefaultAPPInfo() { + String szBranchName = "winboll"; + APPInfo appInfo = new APPInfo(); + appInfo.setAppName("WinBoLL"); + appInfo.setAppIcon(cc.winboll.studio.libaes.R.drawable.ic_winboll); + appInfo.setAppDescription("WinBoLL 网站浏览器。"); + appInfo.setAppGitName("WinBoLL"); + appInfo.setAppGitOwner("Studio"); + appInfo.setAppGitAPPBranch(szBranchName); + appInfo.setAppGitAPPSubProjectFolder(szBranchName); + appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=WinBoLL"); + appInfo.setAppAPKName("WinBoLL"); + appInfo.setAppAPKFolderName("WinBoLL"); + return appInfo; + } + + // ------------------- 新增:对外提供Handler(供其他组件发送消息,如BrowserFragment) ------------------- + public Handler getMainHandler() { + return _mMainHandler; + } + + // ------------------- 新增:生命周期管理(防止Handler内存泄漏) ------------------- + @Override + protected void onDestroy() { + super.onDestroy(); + // 清除Handler所有消息和回调,避免内存泄漏 + if (_mMainHandler != null) { + _mMainHandler.removeCallbacksAndMessages(null); + _mMainHandler = null; + } + } +} + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/MyTileService.java b/winboll/src/main/java/cc/winboll/studio/winboll/MyTileService.java new file mode 100644 index 0000000..f17f234 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/MyTileService.java @@ -0,0 +1,80 @@ +package cc.winboll.studio.winboll; + +/** + * @Author ZhanGSKen + * @Date 2025/02/13 19:30:10 + */ +import android.content.Context; +import cc.winboll.studio.winboll.R; +import android.service.quicksettings.Tile; +import android.service.quicksettings.TileService; +import cc.winboll.studio.winboll.models.MainServiceBean; +import cc.winboll.studio.winboll.services.MainService; + +public class MyTileService extends TileService { + public static final String TAG = "MyTileService"; + + volatile static MyTileService _MyTileService; + + @Override + public void onStartListening() { + super.onStartListening(); + _MyTileService = this; + Tile tile = getQsTile(); + MainServiceBean bean = MainServiceBean.loadBean(this, MainServiceBean.class); + if (bean != null && bean.isEnable()) { + //MainService.startMainService(context); + tile.setState(Tile.STATE_ACTIVE); + tile.setIcon(android.graphics.drawable.Icon.createWithResource(this, R.drawable.ic_cloud)); + } else { + //MainService.stopMainService(context); + tile.setState(Tile.STATE_INACTIVE); + tile.setIcon(android.graphics.drawable.Icon.createWithResource(this, R.drawable.ic_cloud_outline)); + } + tile.updateTile(); +// Tile tile = getQsTile(); +// tile.setState(Tile.STATE_INACTIVE); +// tile.setLabel(getString(R.string.tileservice_name)); +// tile.setIcon(android.graphics.drawable.Icon.createWithResource(this, R.drawable.ic_cloud_outline)); +// tile.updateTile(); + + } + + @Override + public void onClick() { + super.onClick(); + Tile tile = getQsTile(); + MainServiceBean bean = MainServiceBean.loadBean(this, MainServiceBean.class); + if (bean == null) { + bean = new MainServiceBean(); + } + + if (tile.getState() == Tile.STATE_ACTIVE) { + bean.setIsEnable(false); + MainServiceBean.saveBean(this, bean); + MainService.stopMainService(this); + } else if (tile.getState() == Tile.STATE_INACTIVE) { + bean.setIsEnable(true); + MainServiceBean.saveBean(this, bean); + MainService.startMainService(this); + } + updateServiceIconStatus(this); + } + + public static void updateServiceIconStatus(Context context) { + if (_MyTileService == null) { + return; + } + + Tile tile = _MyTileService.getQsTile(); + MainServiceBean bean = MainServiceBean.loadBean(context, MainServiceBean.class); + if (bean != null && bean.isEnable()) { + tile.setState(Tile.STATE_ACTIVE); + tile.setIcon(android.graphics.drawable.Icon.createWithResource(context, R.drawable.ic_cloud)); + } else { + tile.setState(Tile.STATE_INACTIVE); + tile.setIcon(android.graphics.drawable.Icon.createWithResource(context, R.drawable.ic_cloud_outline)); + } + tile.updateTile(); + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/WinBoLL.java b/winboll/src/main/java/cc/winboll/studio/winboll/WinBoLL.java new file mode 100644 index 0000000..cbdff70 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/WinBoLL.java @@ -0,0 +1,40 @@ +package cc.winboll.studio.winboll; + +/** + * @Author ZhanGSKen + * @Date 2025/05/10 10:13 + * @Describe WinBoLL 系列应用通用管理类 + */ +import android.content.Context; +import android.content.Intent; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.winboll.models.WinBoLLModel; + +public class WinBoLL { + + public static final String TAG = "WinBoLL"; + + public static final String ACTION_BIND = WinBoLL.class.getName() + ".ACTION_BIND"; + public static final String EXTRA_WINBOLLMODEL = "EXTRA_WINBOLLMODEL"; + + public static void bindToAPPBase(Context context, String appMainService) { + LogUtils.d(TAG, "bindToAPPBase(...)"); + String toPackage = "cc.winboll.studio.appbase"; + startBind(context, toPackage, appMainService); + } + + public static void bindToAPPBaseBeta(Context context, String appMainService) { + LogUtils.d(TAG, "bindToAPPBaseBeta(...)"); + String toPackage = "cc.winboll.studio.appbase.beta"; + startBind(context, toPackage, appMainService); + } + + static void startBind(Context context, String toPackage, String appMainService) { + Intent intent = new Intent(ACTION_BIND); + intent.putExtra(EXTRA_WINBOLLMODEL, (new WinBoLLModel(toPackage, appMainService)).toString()); + intent.setPackage(toPackage); + LogUtils.d(TAG, String.format("ACTION_BIND :\nTo Package : %s\nAPP Main Service : %s", toPackage, appMainService)); + context.sendBroadcast(intent); + } + +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/WinBoLLClientService.java b/winboll/src/main/java/cc/winboll/studio/winboll/WinBoLLClientService.java new file mode 100644 index 0000000..c9d29de --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/WinBoLLClientService.java @@ -0,0 +1,21 @@ +package cc.winboll.studio.winboll; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +/** + * @Author ZhanGSKen + * @Date 2025/05/03 19:28 + */ +public class WinBoLLClientService extends Service { + + public static final String TAG = "WinBoLLClientService"; + + @Override + public IBinder onBind(Intent intent) { + + return null; + } + +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/WinBoLLServiceStatusView.java b/winboll/src/main/java/cc/winboll/studio/winboll/WinBoLLServiceStatusView.java new file mode 100644 index 0000000..e939b7e --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/WinBoLLServiceStatusView.java @@ -0,0 +1,38 @@ +package cc.winboll.studio.winboll; +import android.content.Context; +import android.util.AttributeSet; +import android.widget.LinearLayout; + +/** + * @Author ZhanGSKen + * @Date 2025/05/03 19:14 + */ +public class WinBoLLServiceStatusView extends LinearLayout { + + public static final String TAG = "WinBoLLServiceStatusView"; + + public WinBoLLServiceStatusView(Context context) { + super(context); + } + + public WinBoLLServiceStatusView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public WinBoLLServiceStatusView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public WinBoLLServiceStatusView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + + void setServerHost(String szWinBoLLServerHost) { + + } + + void setAuthInfo(String szDevUserName, String szDevUserPassword) { + + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/WxPayConfig.java b/winboll/src/main/java/cc/winboll/studio/winboll/WxPayConfig.java new file mode 100644 index 0000000..69c7493 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/WxPayConfig.java @@ -0,0 +1,28 @@ +package cc.winboll.studio.winboll; + +/** + * 微信支付配置类 + * @Author ZhanGSKen + * @Date 2026/01/07 + */ +public class WxPayConfig { + // ========== 核心修改点:替换为你的服务端地址 ========== + // 服务端IP/域名 + 端口(Docker部署的服务端,需确保安卓端可访问) + public static final String BASE_URL = "https://wxpay.winboll.cc"; + + // 统一下单接口路径(对应服务端的测试接口) + public static final String CREATE_ORDER_URL = BASE_URL + "/pay/createOrder"; + + // 订单查询接口路径 + public static final String QUERY_ORDER_URL = BASE_URL + "/pay/queryOrder"; + + // ========== 固定支付配置 ========== + public static final String ORDER_BODY = "定额测试支付"; // 商品描述 + public static final int TOTAL_FEE = 1; // 固定金额:1分(沙箱环境推荐) + public static final String TRADE_TYPE = "NATIVE"; // 支付类型:二维码 + + // ========== 轮询配置 ========== + public static final long POLL_INTERVAL = 10000; // 轮询间隔:10秒 + public static final long POLL_TIMEOUT = 45000; // 轮询超时:45秒 +} + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/LogonActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/LogonActivity.java new file mode 100644 index 0000000..7097711 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/LogonActivity.java @@ -0,0 +1,150 @@ +package cc.winboll.studio.winboll.activities; + +import android.app.Activity; +import android.os.Bundle; +import android.view.View; +import android.widget.RadioButton; +import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; +import cc.winboll.studio.libappbase.BuildConfig; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.LogView; +import cc.winboll.studio.winboll.R; +import cc.winboll.studio.winboll.models.UserInfoModel; +import cc.winboll.studio.winboll.utils.RSAUtils; +import cc.winboll.studio.winboll.utils.YunUtils; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * @Author ZhanGSKen + * @Date 2025/06/04 13:29 + * @Describe 用户登录框 + */ +public class LogonActivity extends Activity implements IWinBoLLActivity { + + public static final String TAG = "LogonActivity"; + + public static final String DEBUG_HOST = "http://10.8.0.250:456"; + public static final String YUN_HOST = "https://yun.winboll.cc"; + + + String mHost = ""; + RadioButton mrbYunHost; + RadioButton mrbDebugHost; + LogView mLogView; + + @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_logon); + mLogView = findViewById(R.id.logview); + mLogView.start(); + + mHost = BuildConfig.DEBUG ? DEBUG_HOST: YUN_HOST; + if (BuildConfig.DEBUG) { + mrbYunHost = findViewById(R.id.rb_yunhost); + mrbDebugHost = findViewById(R.id.rb_debughost); + mrbYunHost.setChecked(!BuildConfig.DEBUG); + mrbDebugHost.setChecked(BuildConfig.DEBUG); + } else { + findViewById(R.id.ll_hostbar).setVisibility(View.GONE); + } + } + + public void onSwitchHost(View view) { + if (view.getId() == R.id.rb_yunhost) { + mrbDebugHost.setChecked(false); + mHost = YUN_HOST; + } else if (view.getId() == R.id.rb_debughost) { + mrbYunHost.setChecked(false); + mHost = DEBUG_HOST; + } + } + + @Override + protected void onResume() { + super.onResume(); + mLogView.start(); + } + + public void onTestLogin(View view) { + LogUtils.d(TAG, "onTestLogin"); + final YunUtils yunUtils = YunUtils.getInstance(this); + + UserInfoModel userInfoModel = new UserInfoModel(); + userInfoModel.setUsername("jian"); + userInfoModel.setPassword("kkiio"); + userInfoModel.setToken("aaa111"); + yunUtils.login(mHost, userInfoModel); + } + + public void onTestRSA(View view) { + LogUtils.d(TAG, "onTestRSA"); + RSAUtils utils = RSAUtils.getInstance(this); + + try { + // 测试 1:首次生成密钥对 + LogUtils.d(TAG, "==== 首次生成密钥对 ===="); + if (utils.keysExist()) { + LogUtils.d(TAG, "密钥对已生成"); + } else { + utils.generateAndSaveKeys(); + LogUtils.d(TAG, "密钥对生成成功。"); + } + + // 测试 2:获取密钥对(自动读取已生成的文件) + KeyPair keyPair = utils.getOrGenerateKeys(); + PublicKey publicKey = keyPair.getPublic(); + PrivateKey privateKey = keyPair.getPrivate(); + + // 打印密钥信息 + LogUtils.d(TAG, "\n==== 密钥信息 ===="); + LogUtils.d(TAG, "公钥算法:" + publicKey.getAlgorithm()); + LogUtils.d(TAG, "公钥编码长度:" + publicKey.getEncoded().length + "字节"); + LogUtils.d(TAG, "私钥算法:" + privateKey.getAlgorithm()); + LogUtils.d(TAG, "私钥编码长度:" + privateKey.getEncoded().length + "字节"); + + // 测试 3:重复调用时检查是否复用文件 + LogUtils.d(TAG, "\n==== 二次调用 ===="); + KeyPair reusedPair = utils.getOrGenerateKeys(); + LogUtils.d(TAG, "是否为同一公钥:" + (publicKey.equals(reusedPair.getPublic()))); // true(单例引用) + LogUtils.d(TAG, "操作完成"); + + String testMessage = "Hello, RSA Encryption!"; + + // 1. 获取或生成密钥对 + PublicKey publicKeyReused = reusedPair.getPublic(); + PrivateKey privateKeyReused = reusedPair.getPrivate(); + + // 2. 公钥加密 + byte[] encryptedData = utils.encryptWithPublicKey(testMessage, publicKeyReused); + LogUtils.d(TAG, "加密后数据(字节长度):" + encryptedData.length); + + // 3. 私钥解密 + String decryptedMessage = utils.decryptWithPrivateKey(encryptedData, privateKeyReused); + LogUtils.d(TAG, "解密结果: " + decryptedMessage); + + // 4. 验证解密是否成功 + if (testMessage.equals(decryptedMessage)) { + LogUtils.d(TAG, "加密解密测试通过!"); + } else { + LogUtils.d(TAG, "测试失败:内容不一致"); + } + } catch (Exception e) { + LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); + } + } + + +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/New2Activity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/New2Activity.java new file mode 100644 index 0000000..21620b5 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/New2Activity.java @@ -0,0 +1,65 @@ +package cc.winboll.studio.winboll.activities; + +/** + * @Author ZhanGSKen + * @Date 2025/03/25 11:46:40 + * @Describe 测试窗口2 + */ +import android.app.Activity; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Toolbar; +import cc.winboll.studio.winboll.R; + +public class New2Activity extends WinBoLLActivity { + + public static final String TAG = "New2Activity"; + + Toolbar mToolbar; + //LogView mLogView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_new2); + +// mLogView = findViewById(R.id.logview); +// mLogView.start(); + mToolbar = findViewById(R.id.toolbar); + setActionBar(mToolbar); + + } + + @Override + protected void onResume() { + super.onResume(); + //mLogView.start(); + } + + public void onCloseThisActivity(View view) { + //WinBoLLActivityManager.getInstance().finish(this); + } + + public void onCloseAllActivity(View view) { + //WinBoLLActivityManager.getInstance().finishAll(); + } + + public void onNewActivity(View view) { + //WinBoLLActivityManager.getInstance().startWinBoLLActivity(this, NewActivity.class); + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + //getMenuInflater().inflate(R.menu.toolbar_main, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。 + return super.onOptionsItemSelected(item); + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/NewActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/NewActivity.java new file mode 100644 index 0000000..a120f7b --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/NewActivity.java @@ -0,0 +1,77 @@ +package cc.winboll.studio.winboll.activities; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/03/25 05:04:22 + * @Describe + */ +import android.app.Activity; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Toolbar; +import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; +import cc.winboll.studio.libaes.utils.WinBoLLActivityManager; +import cc.winboll.studio.winboll.R; +import cc.winboll.studio.winboll.App; + +public class NewActivity extends WinBoLLActivity implements IWinBoLLActivity { + + public static final String TAG = "NewActivity"; + + Toolbar mToolbar; + //LogView mLogView; + + @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_new); +// mLogView = findViewById(R.id.logview); +// mLogView.start(); + mToolbar = findViewById(R.id.toolbar); + setActionBar(mToolbar); + + } + + @Override + protected void onResume() { + super.onResume(); + //mLogView.start(); + } + + public void onCloseThisActivity(View view) { + WinBoLLActivityManager.getInstance().finish(this); + } + + public void onCloseAllActivity(View view) { + WinBoLLActivityManager.getInstance().finishAll(); + } + + public void onNew2Activity(View view) { + // WinBoLLActivityManager.getInstance().startWinBoLLActivity(App.getInstance(), New2Activity.class); + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + //getMenuInflater().inflate(R.menu.toolbar_main, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。 + return super.onOptionsItemSelected(item); + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/SettingsActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/SettingsActivity.java new file mode 100644 index 0000000..19a2908 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/SettingsActivity.java @@ -0,0 +1,50 @@ +package cc.winboll.studio.winboll.activities; + +import android.app.Activity; +import android.os.Bundle; +import android.view.View; +import androidx.appcompat.widget.Toolbar; +import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; +import cc.winboll.studio.libaes.utils.AESThemeUtil; +import cc.winboll.studio.winboll.R; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/12/05 18:48 + * @Describe Settings Activity + */ +public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivity { + + public static final String TAG = "SettingsActivity"; + + @Override + public Activity getActivity() { + return this; + } + + @Override + public String getTag() { + return TAG; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + AESThemeUtil.applyAppTheme(this); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + + // 设置工具栏 + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setSubtitle(TAG); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + toolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); // 点击导航栏返回按钮,触发 finish() + } + }); + + } + +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/ShortcutActionActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/ShortcutActionActivity.java new file mode 100644 index 0000000..8ebd744 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/ShortcutActionActivity.java @@ -0,0 +1,50 @@ +package cc.winboll.studio.winboll.activities; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import cc.winboll.studio.libappbase.ToastUtils; +import cc.winboll.studio.winboll.App; +import cc.winboll.studio.winboll.R; +import cc.winboll.studio.winboll.utils.APPPlusUtils; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/27 09:00 + * @Describe 应用快捷方式活动类 + */ +public class ShortcutActionActivity extends Activity { + + public static final String TAG = "ShortcutActionActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // 处理应用级别的切换请求 + handleSwitchRequest(); + finish(); + } + + /** + * 处理应用图标快捷菜单的请求 + */ + private void handleSwitchRequest() { + //ToastUtils.show("handleSwitchRequest"); + Intent intent = getIntent(); + if (intent != null && "switchto_en1".equals(intent.getDataString())) { + APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_EN1); + ToastUtils.show("切换至" + getString(R.string.app_name) + "图标"); + //moveTaskToBack(true); + } + if (intent != null && "switchto_cn1".equals(intent.getDataString())) { + APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_CN1); + ToastUtils.show("切换至" + getString(R.string.app_name_cn1) + "图标"); + //moveTaskToBack(true); + } + if (intent != null && "switchto_cn2".equals(intent.getDataString())) { + APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_CN2); + ToastUtils.show("切换至" + getString(R.string.app_name_cn2) + "图标"); + //moveTaskToBack(true); + } + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/WXPayActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/WXPayActivity.java new file mode 100644 index 0000000..e881993 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/WXPayActivity.java @@ -0,0 +1,211 @@ +package cc.winboll.studio.winboll.activities; + +import android.app.Activity; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; +import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; +import cc.winboll.studio.winboll.R; +import cc.winboll.studio.winboll.WxPayConfig; +import cc.winboll.studio.winboll.utils.SpecUtil; +import cc.winboll.studio.winboll.utils.WxPayApi; +import cc.winboll.studio.winboll.utils.ZXingUtils; +import java.util.Timer; +import java.util.TimerTask; + + + +/** + * 主界面:生成二维码 + 轮询查询支付结果 + * @Author ZhanGSKen + * @Date 2026/01/07 + */ +public class WXPayActivity extends WinBoLLActivity implements IWinBoLLActivity { + + private static final String TAG = "WXPayActivity"; + + // Handler消息标识 + private static final int MSG_POLL_TIMEOUT = 1001; + private static final int MSG_POLL_SUCCESS = 1002; + private static final int MSG_POLL_FAILED = 1003; + + private ImageView mIvQrCode; + private TextView mTvOrderNo; + private TextView mTvPayStatus; + private Button mBtnCreateOrder; + + private String mOutTradeNo; // 商户订单号 + private Timer mPollTimer; // 轮询定时器 + private long mPollStartTime; // 轮询开始时间 + + + + @Override + public Activity getActivity() { + return this; + } + + @Override + public String getTag() { + return TAG; + } + + private Handler mPollHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + super.handleMessage(msg); + switch (msg.what) { + case MSG_POLL_TIMEOUT: + stopPoll(); + mTvPayStatus.setText("支付状态:轮询超时"); + mTvPayStatus.setTextColor(getResources().getColor(android.R.color.darker_gray)); + Toast.makeText(WXPayActivity.this, "轮询超时,请手动查询", Toast.LENGTH_SHORT).show(); + break; + case MSG_POLL_SUCCESS: + boolean isPaySuccess = (boolean) msg.obj; + String tradeState = (String) msg.getData().getString("tradeState"); + stopPoll(); + if (isPaySuccess) { + mTvPayStatus.setText("支付状态:支付成功 ✅"); + mTvPayStatus.setTextColor(getResources().getColor(android.R.color.holo_green_dark)); + Toast.makeText(WXPayActivity.this, "支付成功!", Toast.LENGTH_SHORT).show(); + } else { + mTvPayStatus.setText("支付状态:" + tradeState); + mTvPayStatus.setTextColor(getResources().getColor(android.R.color.holo_red_dark)); + } + break; + case MSG_POLL_FAILED: + String errorMsg = (String) msg.obj; + mTvPayStatus.setText("查询失败:" + errorMsg); + break; + } + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_wxpay); + + initView(); + initListener(); + } + + private void initView() { + mIvQrCode = findViewById(R.id.iv_qrcode); + mTvOrderNo = findViewById(R.id.tv_order_no); + mTvPayStatus = findViewById(R.id.tv_pay_status); + mBtnCreateOrder = findViewById(R.id.btn_create_order); + } + + private void initListener() { + mBtnCreateOrder.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + createOrder(); + } + }); + } + + /** + * 统一下单,生成二维码 + */ + private void createOrder() { + mBtnCreateOrder.setEnabled(false); + mTvPayStatus.setText("支付状态:生成订单中..."); + mIvQrCode.setImageBitmap(null); + + WxPayApi.createOrder(new WxPayApi.OnCreateOrderCallback() { + @Override + public void onSuccess(String outTradeNo, String codeUrl) { + mOutTradeNo = outTradeNo; + mTvOrderNo.setText("商户订单号:" + outTradeNo); + mTvPayStatus.setText("支付状态:未支付,请扫码"); + + // 生成二维码 + Bitmap qrCodeBitmap = ZXingUtils.createQRCodeBitmap(codeUrl, 250, 250); + if (qrCodeBitmap != null) { + mIvQrCode.setImageBitmap(qrCodeBitmap); + // 开始轮询查询支付结果 + startPoll(); + } else { + mTvPayStatus.setText("支付状态:生成二维码失败"); + mBtnCreateOrder.setEnabled(true); + } + } + + @Override + public void onFailure(String errorMsg) { + SpecUtil.WWSpecLogError(TAG, "统一下单失败:" + errorMsg); + mTvPayStatus.setText("生成订单失败:" + errorMsg); + mBtnCreateOrder.setEnabled(true); + Toast.makeText(WXPayActivity.this, errorMsg, Toast.LENGTH_SHORT).show(); + } + }); + } + + /** + * 开始轮询查询支付结果 + */ + private void startPoll() { + stopPoll(); // 先停止之前的轮询 + mPollStartTime = System.currentTimeMillis(); + mPollTimer = new Timer(); + mPollTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + // 检查是否超时 + long elapsedTime = System.currentTimeMillis() - mPollStartTime; + if (elapsedTime >= WxPayConfig.POLL_TIMEOUT) { + mPollHandler.sendEmptyMessage(MSG_POLL_TIMEOUT); + return; + } + + // 查询订单状态 + WxPayApi.queryOrder(mOutTradeNo, new WxPayApi.OnQueryOrderCallback() { + @Override + public void onSuccess(boolean isPaySuccess, String tradeState) { + Message msg = Message.obtain(); + msg.what = MSG_POLL_SUCCESS; + msg.obj = isPaySuccess; + Bundle bundle = new Bundle(); + bundle.putString("tradeState", tradeState); + msg.setData(bundle); + mPollHandler.sendMessage(msg); + } + + @Override + public void onFailure(String errorMsg) { + Message msg = Message.obtain(); + msg.what = MSG_POLL_FAILED; + msg.obj = errorMsg; + mPollHandler.sendMessage(msg); + } + }); + } + }, 0, WxPayConfig.POLL_INTERVAL); + } + + /** + * 停止轮询 + */ + private void stopPoll() { + if (mPollTimer != null) { + mPollTimer.cancel(); + mPollTimer = null; + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + stopPoll(); + } +} + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/WinBoLLActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/WinBoLLActivity.java new file mode 100644 index 0000000..8f47d37 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/WinBoLLActivity.java @@ -0,0 +1,47 @@ +package cc.winboll.studio.winboll.activities; + +/** + * @Author ZhanGSKen + * @Date 2025/05/10 09:48 + * @Describe WinBoLL 窗口基础类 + */ +import android.app.Activity; +import android.os.Bundle; +import android.view.MenuItem; +import androidx.appcompat.app.AppCompatActivity; +import cc.winboll.studio.libappbase.LogUtils; + +public class WinBoLLActivity extends AppCompatActivity { + + public static final String TAG = "WinBoLLActivity"; + + @Override + protected void onResume() { + super.onResume(); + } + + @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().registeRemove(this); + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/WinBoLLUnitTestActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/WinBoLLUnitTestActivity.java new file mode 100644 index 0000000..177c5a0 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/WinBoLLUnitTestActivity.java @@ -0,0 +1,172 @@ +package cc.winboll.studio.winboll.activities; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Intent; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.CheckBox; +import cc.winboll.studio.libaes.utils.WinBoLLActivityManager; +import cc.winboll.studio.libappbase.CrashHandler; +import cc.winboll.studio.libappbase.GlobalApplication; +import cc.winboll.studio.libappbase.GlobalCrashActivity; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.ToastUtils; +import cc.winboll.studio.winboll.App; +import cc.winboll.studio.winboll.R; +import cc.winboll.studio.winboll.services.MainService; +import cc.winboll.studio.winboll.services.TestDemoBindService; +import cc.winboll.studio.winboll.services.TestDemoService; +import cc.winboll.studio.winboll.sos.SOS; +import cc.winboll.studio.winboll.widgets.StatusWidget; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; + +public class WinBoLLUnitTestActivity extends AppCompatActivity { + + public static final String TAG = "MainActivity"; + + Toolbar mToolbar; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ToastUtils.show("onCreate"); + setContentView(R.layout.activity_winbollunittest); + + mToolbar = findViewById(R.id.toolbar); + setSupportActionBar(mToolbar); + + CheckBox cbIsDebugMode = findViewById(R.id.activitymainCheckBox1); + cbIsDebugMode.setChecked(GlobalApplication.isDebugging()); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + //getMenuInflater().inflate(R.menu.toolbar_main, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { +// if(item.getItemId() == R.id.item_yun) { +// WinBoLLActivityManager.getInstance().startWinBoLLActivity(this, cc.winboll.studio.winboll.activities.YunActivity.class); +// } else if(item.getItemId() == R.id.item_logon) { +// WinBoLLActivityManager.getInstance().startWinBoLLActivity(this, cc.winboll.studio.winboll.activities.LogonActivity.class); +// } + // 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。 + return super.onOptionsItemSelected(item); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + Intent intentAPPWidget = new Intent(this, StatusWidget.class); + intentAPPWidget.setAction(StatusWidget.ACTION_STATUS_UPDATE); + sendBroadcast(intentAPPWidget); + } + + public void onSwitchDebugMode(View view) { + boolean isDebuging = ((CheckBox)view).isChecked(); + GlobalApplication.setIsDebugging(isDebuging); + GlobalApplication.saveDebugStatus((App.getInstance())); + } + + public void onPreviewGlobalCrashActivity(View view) { + Intent intent = new Intent(this, GlobalCrashActivity.class); + intent.putExtra(CrashHandler.EXTRA_CRASH_LOG, "Demo log..."); + startActivity(intent); + } + + public void onStartCenter(View view) { + MainService.startMainService(this); + } + + public void onStopCenter(View view) { + MainService.stopMainService(this); + } + + public void onTestStopMainServiceWithoutSettingEnable(View view) { + LogUtils.d(TAG, "onTestStopMainServiceWithoutSettingEnable"); + stopService(new Intent(this, MainService.class)); + } + + public void onTestUseComponentStartService(View view) { + LogUtils.d(TAG, "onTestUseComponentStartService"); + + // 目标服务的包名和类名 + String packageName = this.getPackageName(); + String serviceClassName = TestDemoService.class.getName(); + + // 构建Intent + Intent intentService = new Intent(); + intentService.setComponent(new ComponentName(packageName, serviceClassName)); + + startService(intentService); + } + + public void onTestDemoServiceSOS(View view) { + Intent intent = new Intent(this, TestDemoService.class); + stopService(intent); + if (App.isDebugging()) { + SOS.sosToAppBaseBeta(this, TestDemoService.class.getName()); + } else { + SOS.sosToAppBase(this, TestDemoService.class.getName()); + } + } + + public void onSartTestDemoService(View view) { + Intent intent = new Intent(this, TestDemoService.class); + intent.setAction(TestDemoService.ACTION_ENABLE); + startService(intent); + + } + + public void onStopTestDemoService(View view) { + Intent intent = new Intent(this, TestDemoService.class); + intent.setAction(TestDemoService.ACTION_DISABLE); + startService(intent); + + Intent intentStop = new Intent(this, TestDemoService.class); + stopService(intentStop); + } + + public void onStopTestDemoServiceNoSettings(View view) { + Intent intent = new Intent(this, TestDemoService.class); + stopService(intent); + } + + public void onSartTestDemoBindService(View view) { + Intent intent = new Intent(this, TestDemoBindService.class); + intent.setAction(TestDemoBindService.ACTION_ENABLE); + startService(intent); + + } + + public void onStopTestDemoBindService(View view) { + Intent intent = new Intent(this, TestDemoBindService.class); + intent.setAction(TestDemoBindService.ACTION_DISABLE); + startService(intent); + + Intent intentStop = new Intent(this, TestDemoBindService.class); + stopService(intentStop); + } + + public void onStopTestDemoBindServiceNoSettings(View view) { + Intent intent = new Intent(this, TestDemoBindService.class); + stopService(intent); + } + + public void onTestOpenNewActivity(View view) { + WinBoLLActivityManager.getInstance().startWinBoLLActivity(this, NewActivity.class); + } + + @Override + protected void onResume() { + super.onResume(); + } + + +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/YunActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/YunActivity.java new file mode 100644 index 0000000..8ab5fb6 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/YunActivity.java @@ -0,0 +1,126 @@ +package cc.winboll.studio.winboll.activities; + +/** + * @Author ZhanGSKen + * @Date 2025/06/04 11:06 + * @Describe 云宝云 + */ +import android.app.Activity; +import android.os.Bundle; +import android.view.View; +import android.widget.RadioButton; +import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; +import cc.winboll.studio.libappbase.BuildConfig; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.LogView; +import cc.winboll.studio.winboll.R; +import java.io.IOException; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +public class YunActivity extends Activity implements IWinBoLLActivity { + + public static final String TAG = "YunActivity"; + + public static final String DEBUG_HOST = "http://10.8.0.250:456"; + public static final String YUN_HOST = "https://yun.winboll.cc"; + + String mHost = ""; + RadioButton mrbYunHost; + RadioButton mrbDebugHost; + LogView mLogView; + + @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_yun); + mLogView = findViewById(R.id.logview); + mLogView.start(); + + mHost = BuildConfig.DEBUG ? DEBUG_HOST: YUN_HOST; + if (BuildConfig.DEBUG) { + mrbYunHost = findViewById(R.id.rb_yunhost); + mrbDebugHost = findViewById(R.id.rb_debughost); + mrbYunHost.setChecked(!BuildConfig.DEBUG); + mrbDebugHost.setChecked(BuildConfig.DEBUG); + } else { + findViewById(R.id.ll_hostbar).setVisibility(View.GONE); + } + } + + public void onSwitchHost(View view) { + if (view.getId() == R.id.rb_yunhost) { + mrbDebugHost.setChecked(false); + mHost = YUN_HOST; + } else if (view.getId() == R.id.rb_debughost) { + mrbYunHost.setChecked(false); + mHost = DEBUG_HOST; + } + } + + @Override + protected void onResume() { + super.onResume(); + mLogView.start(); + } + + public void onTestYun(View view) { + LogUtils.d(TAG, "onTestYun"); + (new Thread(new Runnable(){ + @Override + public void run() { + testYun(); + } + })).start(); + } + + void testYun() { + OkHttpClient client = new OkHttpClient(); + Request request = new Request.Builder() + .url(mHost + "/backups/") + .build(); + + Response response = null; + try { + response = client.newCall(request).execute(); + if (response.isSuccessful()) { + String responseBody = ""; + if (response.body() != null) { + responseBody = response.body().string(); + } + + // 正则匹配:任意主机名 -> Test OK(主机名部分匹配非空字符) + boolean isMatch = responseBody.matches(".+? -> Test OK"); + + if (isMatch) { + LogUtils.d(TAG, responseBody); + } else { + LogUtils.d(TAG, "响应内容不匹配,内容:" + responseBody); + } + } else { + LogUtils.d(TAG, "请求失败,状态码:" + response.code()); + } + } catch (IOException e) { + LogUtils.d(TAG, "读取响应体失败:" + e.getMessage()); + } catch (Exception e) { + LogUtils.d(TAG, "异常:" + e.getMessage()); + e.printStackTrace(); // Java 7 需显式打印堆栈 + } finally { + // 手动关闭 Response(Java 7 不支持 try-with-resources) + if (response != null && response.body() != null) { + response.body().close(); + } + } + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/fragments/BrowserFragment.java b/winboll/src/main/java/cc/winboll/studio/winboll/fragments/BrowserFragment.java new file mode 100644 index 0000000..ee58f42 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/fragments/BrowserFragment.java @@ -0,0 +1,319 @@ +package cc.winboll.studio.winboll.fragments; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import cc.winboll.studio.winboll.MainActivity; +import cc.winboll.studio.winboll.R; +import cc.winboll.studio.winboll.views.WinBoLLView; +import java.util.ArrayList; +import android.app.Activity; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/27 11:09 + * @Describe 浏览器Fragment(Java 7 语法完整版,新增Handler消息接收) + * 适配Java 7特性,支持接收应用内消息(如MSG_HOMEPAGE跳转首页) + */ +public class BrowserFragment extends Fragment implements View.OnClickListener, WinBoLLView.OnPageStatusListener { + + // 控件声明(Java 7 成员变量显式声明) + private EditText mEtUrl; + private Button mBtnLoad; + private Button mBtnRefresh; + private Button mBtnStop; + private Button mBtnForward; + private Button mBtnBack; + private ProgressBar mProgressBar; + private WinBoLLView mWinBoLLView; + public static ArrayList _mUrlLoadHistory = new ArrayList(); + + // ------------------- 新增:Handler 消息定义(应用内通信) ------------------- + // 消息标识:跳转首页(winboll.cc) + public static final int MSG_HOMEPAGE = 1001; + // 跳转到历史记录位置 + public static final int MSG_HISTORY_POSITION = 1002; + // 自定义Handler(接收应用内其他页面发送的消息) + private Handler mBrowserHandler; + + // 单例创建方法(Java 7 静态工厂模式) + public static BrowserFragment newInstance() { + return new BrowserFragment(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + // 加载布局(Java 7 显式强转,无菱形语法) + View view = inflater.inflate(R.layout.fragment_browser, container, false); + // 清理旧历史记录 + _mUrlLoadHistory.clear(); + + // 初始化控件 + initViews(view); + // 绑定事件 + initEvents(); + // 初始化WinBoLLView + initWinBoLLView(); + // ------------------- 新增:初始化Handler(关键) ------------------- + initHandler(); + return view; + } + + /** + * 初始化控件(Java 7 显式绑定,无Stream简化) + */ + private void initViews(View view) { + mEtUrl = (EditText) view.findViewById(R.id.et_url); + mBtnLoad = (Button) view.findViewById(R.id.btn_load); + mBtnRefresh = (Button) view.findViewById(R.id.btn_refresh); + mBtnStop = (Button) view.findViewById(R.id.btn_stop); + mBtnForward = (Button) view.findViewById(R.id.btn_forward); + mBtnBack = (Button) view.findViewById(R.id.btn_back); + mProgressBar = (ProgressBar) view.findViewById(R.id.progress_bar); + mWinBoLLView = (WinBoLLView) view.findViewById(R.id.winboll_webview); + } + + /** + * 绑定点击事件(Java 7 匿名内部类,无Lambda) + */ + private void initEvents() { + // 功能按钮点击事件 + mBtnLoad.setOnClickListener(this); + mBtnRefresh.setOnClickListener(this); + mBtnStop.setOnClickListener(this); + mBtnForward.setOnClickListener(this); + mBtnBack.setOnClickListener(this); + + // 输入框软键盘“前往”按钮事件(Java 7 匿名内部类实现) + mEtUrl.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, android.view.KeyEvent event) { + // 处理软键盘“前往”点击 + loadUrlFromInput(); + return true; + } + }); + } + + /** + * 初始化WinBoLLView(Java 7 显式调用,无方法引用) + */ + private void initWinBoLLView() { + // 绑定进度条 + mWinBoLLView.setProgressBar(mProgressBar); + // 设置页面状态监听(this 实现 OnPageStatusListener) + mWinBoLLView.setOnPageStatusListener(this); + // 预加载默认页面(winboll.cc 首页) + String defaultUrl = "https://www.winboll.cc"; + mWinBoLLView.loadUrlSafe(defaultUrl); + mEtUrl.setText(defaultUrl); + } + + // ------------------- 新增:初始化Handler(接收应用内消息) ------------------- + private void initHandler() { + // Java 7 匿名内部类实现Handler(主线程中创建,用于更新UI) + mBrowserHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + super.handleMessage(msg); + // 根据消息标识处理不同逻辑 + switch (msg.what) { + case MSG_HOMEPAGE: + // 处理“跳转首页”消息:加载winboll.cc + String homeUrl = "https://www.winboll.cc"; + mWinBoLLView.loadUrlSafe(homeUrl); + mEtUrl.setText(homeUrl); + showToast("已跳转至首页"); + break; + case MSG_HISTORY_POSITION: + int position = (int)msg.obj; + if(-1 < position && position < _mUrlLoadHistory.size()) { + // 处理“跳转首页”消息:加载winboll.cc + String historyUrl = _mUrlLoadHistory.get(position); + mWinBoLLView.loadUrlSafe(historyUrl); + mEtUrl.setText(historyUrl); + //showToast("已跳转至" + historyUrl); + } + break; + default: + break; + } + } + }; + } + + /** + * 点击事件处理(Java 7 switch-case 语句,无增强switch) + */ + @Override + public void onClick(View v) { + int id = v.getId(); + if (id == R.id.btn_load) { + // 加载输入框中的URL + loadUrlFromInput(); + } else if (id == R.id.btn_refresh) { + // 刷新当前页面 + mWinBoLLView.refreshPage(); + } else if (id == R.id.btn_stop) { + // 停止页面加载 + mWinBoLLView.stopPageLoad(); + } else if (id == R.id.btn_forward) { + // 前进(无历史则提示) + if (!mWinBoLLView.goForwardSafe()) { + showToast("无前进历史"); + } + } else if (id == R.id.btn_back) { + // 后退(无历史则提示) + if (!mWinBoLLView.goBackSafe()) { + showToast("无后退历史"); + } + } + } + + /** + * 从输入框获取URL并加载(Java 7 显式空值校验) + */ + private void loadUrlFromInput() { + // 空值校验(Java 7 显式判断,无Objects.requireNonNull) + if (mEtUrl == null) { + showToast("控件初始化失败"); + return; + } + String url = mEtUrl.getText().toString().trim(); + // 调用WinBoLLView安全加载方法 + mWinBoLLView.loadUrlSafe(url); + // 隐藏软键盘 + hideSoftKeyboard(); + } + + /** + * 隐藏软键盘(Java 7 显式获取系统服务,无Lambda简化) + */ + private void hideSoftKeyboard() { + if (getActivity() == null) { + return; + } + // 获取InputMethodManager(Java 7 显式强转) + InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(android.content.Context.INPUT_METHOD_SERVICE); + if (imm != null && getActivity().getCurrentFocus() != null) { + imm.hideSoftInputFromWindow(getActivity().getCurrentFocus().getWindowToken(), 0); + } + } + + /** + * 显示Toast提示(Java 7 简化封装,避免重复代码) + */ + private void showToast(String msg) { + if (getActivity() == null || msg == null) { + return; + } + // Java 7 显式创建Toast,无Toast.makeText简化链式调用 + Toast toast = Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT); + toast.show(); + } + + // ------------------- WinBoLLView.OnPageStatusListener 实现(Java 7 显式重写) ------------------- + @Override + public void onPageStarted(String url) { + // 页面开始加载:更新输入框URL(Java 7 显式非空判断) + if (mEtUrl != null && url != null) { + mEtUrl.setText(url); + } + } + + @Override + public void onPageFinished(String url) { + // 页面加载完成:更新输入框URL + if (mEtUrl != null && url != null) { + mEtUrl.setText(url); + addUrlToHistory(url); + } + } + + @Override + public void onPageError(String errorMsg) { + // 页面加载错误:显示错误提示 + showToast("加载失败:" + errorMsg); + } + + // ------------------- 新增:对外提供Handler(供其他页面获取并发送消息) ------------------- + public Handler getBrowserHandler() { + return mBrowserHandler; + } + + // ------------------- 生命周期管理(防止内存泄漏,Java 7 显式重写) ------------------- + @Override + public void onDestroyView() { + super.onDestroyView(); + // 销毁WinBoLLView(释放资源,避免内存泄漏) + if (mWinBoLLView != null) { + mWinBoLLView.destroyWebView(); + mWinBoLLView = null; + } + // ------------------- 新增:移除Handler消息(关键,防止内存泄漏) ------------------- + if (mBrowserHandler != null) { + mBrowserHandler.removeCallbacksAndMessages(null); // 清除所有消息和回调 + mBrowserHandler = null; + } + // 置空控件(帮助GC回收) + mEtUrl = null; + mBtnLoad = null; + mBtnRefresh = null; + mBtnStop = null; + mBtnForward = null; + mBtnBack = null; + mProgressBar = null; + } + + @Override + public void onDestroy() { + super.onDestroy(); + // 彻底释放资源 + if (mWinBoLLView != null) { + mWinBoLLView.destroyWebView(); + mWinBoLLView = null; + } + // 再次清除Handler(双重保险) + if (mBrowserHandler != null) { + mBrowserHandler.removeCallbacksAndMessages(null); + mBrowserHandler = null; + } + } + + // 在 BrowserFragment 中添加以下代码(Java 7 语法) + /** + * 发送URL历史更新消息给MainActivity(当历史列表变化时调用) + */ + private void sendUrlHistoryUpdateMsg() { + Message msg = Message.obtain(); + msg.what = MainActivity.MSG_URLLOADHISTORY_UPDATE; + MainActivity.sendMessage(msg); + } + + // 调用时机示例(在BrowserFragment加载URL并更新历史列表后调用) + // 假设BrowserFragment中有添加URL到历史的方法: + private void addUrlToHistory(String url) { + if (_mUrlLoadHistory == null) { + _mUrlLoadHistory = new ArrayList(); + } + if (!_mUrlLoadHistory.contains(url)) { + _mUrlLoadHistory.add(0, url); + // 关键:添加历史后发送更新消息,通知MainActivity刷新抽屉菜单 + sendUrlHistoryUpdateMsg(); + } + } +} + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/fragments/MainFragment.java b/winboll/src/main/java/cc/winboll/studio/winboll/fragments/MainFragment.java new file mode 100644 index 0000000..4610993 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/fragments/MainFragment.java @@ -0,0 +1,35 @@ +package cc.winboll.studio.winboll.fragments; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/09/29 13:15 + * @Describe MainFragment + */ +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Switch; +import androidx.fragment.app.Fragment; +import cc.winboll.studio.libappbase.ToastUtils; +import cc.winboll.studio.winboll.R; + + +public class MainFragment extends Fragment { + + public static final String TAG = "MainFragment"; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_main, container, false); + Switch swEnablePosition = view.findViewById(R.id.fragmentmainSwitch1); + swEnablePosition.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + ToastUtils.show("Position"); + } + }); + return view; + } + +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/handlers/MainServiceHandler.java b/winboll/src/main/java/cc/winboll/studio/winboll/handlers/MainServiceHandler.java new file mode 100644 index 0000000..effeb43 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/handlers/MainServiceHandler.java @@ -0,0 +1,38 @@ +package cc.winboll.studio.winboll.handlers; + +/** + * @Author ZhanGSKen + * @Date 2025/02/14 03:51:40 + */ +import android.os.Handler; +import android.os.Message; +import java.lang.ref.WeakReference; +import cc.winboll.studio.winboll.services.MainService; + +public class MainServiceHandler extends Handler { + public static final String TAG = "MainServiceHandler"; + + public static final int MSG_REMINDTHREAD = 0; + + WeakReference serviceWeakReference; + public MainServiceHandler(MainService service) { + serviceWeakReference = new WeakReference(service); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_REMINDTHREAD: // 处理下载完成消息,更新UI + { + // 显示提醒消息 + // + //LogUtils.d(TAG, "显示提醒消息"); + MainService mainService = serviceWeakReference.get(); + if (mainService != null) { + mainService.appenMessage((String)msg.obj); + } + break; + } + } + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/models/MainServiceBean.java b/winboll/src/main/java/cc/winboll/studio/winboll/models/MainServiceBean.java new file mode 100644 index 0000000..98b64a1 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/models/MainServiceBean.java @@ -0,0 +1,67 @@ +package cc.winboll.studio.winboll.models; + +/** + * @Author ZhanGSKen + * @Date 2025/02/13 07:06:13 + */ +import android.util.JsonReader; +import android.util.JsonWriter; +import cc.winboll.studio.libappbase.BaseBean; +import java.io.IOException; + +public class MainServiceBean extends BaseBean { + + public static final String TAG = "MainServiceBean"; + + boolean isEnable; + + public MainServiceBean() { + this.isEnable = false; + } + + public void setIsEnable(boolean isEnable) { + this.isEnable = isEnable; + } + + public boolean isEnable() { + return isEnable; + } + + @Override + public String getName() { + return MainServiceBean.class.getName(); + } + + @Override + public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { + super.writeThisToJsonWriter(jsonWriter); + jsonWriter.name("isEnable").value(isEnable()); + + } + + @Override + public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException { + if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else { + if (name.equals("isEnable")) { + setIsEnable(jsonReader.nextBoolean()); + } else { + return false; + } + } + return true; + } + + @Override + public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { + jsonReader.beginObject(); + while (jsonReader.hasNext()) { + String name = jsonReader.nextName(); + if (!initObjectsFromJsonReader(jsonReader, name)) { + jsonReader.skipValue(); + } + } + // 结束 JSON 对象 + jsonReader.endObject(); + return this; + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/models/ResponseData.java b/winboll/src/main/java/cc/winboll/studio/winboll/models/ResponseData.java new file mode 100644 index 0000000..ed78bdc --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/models/ResponseData.java @@ -0,0 +1,53 @@ +package cc.winboll.studio.winboll.models; + +/** + * @Author ZhanGSKen + * @Date 2025/06/05 11:26 + */ + +public class ResponseData { + + public static final String STATUS_SUCCESS = "success"; + public static final String STATUS_ERROR = "error"; + + private String status; + private String message; + private UserInfoModel data; + + public ResponseData() { + this.status = ""; + this.message = ""; + this.data = new UserInfoModel(); + } + + public ResponseData(String status, String message, UserInfoModel data) { + this.status = status; + this.message = message; + this.data = data; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getStatus() { + return status; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setData(UserInfoModel data) { + this.data = data; + } + + public UserInfoModel getData() { + return data; + } +} + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/models/TestDemoBindServiceBean.java b/winboll/src/main/java/cc/winboll/studio/winboll/models/TestDemoBindServiceBean.java new file mode 100644 index 0000000..ab68051 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/models/TestDemoBindServiceBean.java @@ -0,0 +1,67 @@ +package cc.winboll.studio.winboll.models; + +/** + * @Author ZhanGSKen + * @Date 2025/03/07 12:47:22 + * @Describe TestServiceBean + */ +import android.util.JsonReader; +import android.util.JsonWriter; +import cc.winboll.studio.libappbase.BaseBean; +import java.io.IOException; + +public class TestDemoBindServiceBean extends BaseBean { + + public static final String TAG = "TestServiceBean"; + + boolean isEnable; + + public TestDemoBindServiceBean() { + this.isEnable = false; + } + + public void setIsEnable(boolean isEnable) { + this.isEnable = isEnable; + } + + public boolean isEnable() { + return isEnable; + } + + @Override + public String getName() { + return TestDemoBindServiceBean.class.getName(); + } + + @Override + public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { + super.writeThisToJsonWriter(jsonWriter); + jsonWriter.name("isEnable").value(isEnable()); + } + + @Override + public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException { + if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else { + if (name.equals("isEnable")) { + setIsEnable(jsonReader.nextBoolean()); + } else { + return false; + } + } + return true; + } + + @Override + public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { + jsonReader.beginObject(); + while (jsonReader.hasNext()) { + String name = jsonReader.nextName(); + if (!initObjectsFromJsonReader(jsonReader, name)) { + jsonReader.skipValue(); + } + } + // 结束 JSON 对象 + jsonReader.endObject(); + return this; + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/models/TestDemoServiceBean.java b/winboll/src/main/java/cc/winboll/studio/winboll/models/TestDemoServiceBean.java new file mode 100644 index 0000000..5899eca --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/models/TestDemoServiceBean.java @@ -0,0 +1,68 @@ +package cc.winboll.studio.winboll.models; + +/** + * @Author ZhanGSKen + * @Date 2025/03/07 12:49:21 + * @Describe TestDemoServiceBean + */ +import android.util.JsonReader; +import android.util.JsonWriter; +import cc.winboll.studio.libappbase.BaseBean; +import java.io.IOException; + +public class TestDemoServiceBean extends BaseBean { + + public static final String TAG = "TestDemoServiceBean"; + + boolean isEnable; + + public TestDemoServiceBean() { + this.isEnable = false; + } + + public void setIsEnable(boolean isEnable) { + this.isEnable = isEnable; + } + + public boolean isEnable() { + return isEnable; + } + + @Override + public String getName() { + return TestDemoServiceBean.class.getName(); + } + + @Override + public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { + super.writeThisToJsonWriter(jsonWriter); + jsonWriter.name("isEnable").value(isEnable()); + + } + + @Override + public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException { + if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else { + if (name.equals("isEnable")) { + setIsEnable(jsonReader.nextBoolean()); + } else { + return false; + } + } + return true; + } + + @Override + public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { + jsonReader.beginObject(); + while (jsonReader.hasNext()) { + String name = jsonReader.nextName(); + if (!initObjectsFromJsonReader(jsonReader, name)) { + jsonReader.skipValue(); + } + } + // 结束 JSON 对象 + jsonReader.endObject(); + return this; + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/models/UserInfoModel.java b/winboll/src/main/java/cc/winboll/studio/winboll/models/UserInfoModel.java new file mode 100644 index 0000000..52c4e4c --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/models/UserInfoModel.java @@ -0,0 +1,92 @@ +package cc.winboll.studio.winboll.models; + +/** + * @Author ZhanGSKen + * @Date 2025/06/04 19:14 + */ +import android.util.JsonReader; +import android.util.JsonWriter; +import cc.winboll.studio.libappbase.BaseBean; +import java.io.IOException; + +public class UserInfoModel extends BaseBean { + + public static final String TAG = "UserInfoModel"; + + String username; + String password; + String token; + + public UserInfoModel() { + this.username = ""; + this.password = ""; + this.token = ""; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getUsername() { + return username; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getPassword() { + return password; + } + + public void setToken(String token) { + this.token = token; + } + + public String getToken() { + return token; + } + + @Override + public String getName() { + return UserInfoModel.class.getName(); + } + + @Override + public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { + super.writeThisToJsonWriter(jsonWriter); + jsonWriter.name("username").value(getUsername()); + jsonWriter.name("password").value(getPassword()); + jsonWriter.name("token").value(getToken()); + } + + @Override + public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException { + if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else { + if (name.equals("username")) { + setUsername(jsonReader.nextString()); + } else if (name.equals("password")) { + setPassword(jsonReader.nextString()); + } else if (name.equals("token")) { + setToken(jsonReader.nextString()); + } else { + return false; + } + } + return true; + } + + @Override + public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { + jsonReader.beginObject(); + while (jsonReader.hasNext()) { + String name = jsonReader.nextName(); + if (!initObjectsFromJsonReader(jsonReader, name)) { + jsonReader.skipValue(); + } + } + // 结束 JSON 对象 + jsonReader.endObject(); + return this; + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/models/WinBoLLModel.java b/winboll/src/main/java/cc/winboll/studio/winboll/models/WinBoLLModel.java new file mode 100644 index 0000000..6b5d65d --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/models/WinBoLLModel.java @@ -0,0 +1,92 @@ +package cc.winboll.studio.winboll.models; + +/** + * @Author ZhanGSKen + * @Date 2025/05/10 10:16 + * @Describe WinBoLLModel + */ +import android.util.JsonReader; +import android.util.JsonWriter; +import cc.winboll.studio.libappbase.BaseBean; +import java.io.IOException; +import cc.winboll.studio.libappbase.APPModel; + +public class WinBoLLModel extends BaseBean { + + public static final String TAG = "WinBoLLModel"; + + String appPackageName; + String appMainServiveName; + + public WinBoLLModel() { + this.appPackageName = ""; + this.appMainServiveName = ""; + } + + public WinBoLLModel(boolean isDebuging, String appPackageName, String appMainServiveName) { + this.appPackageName = appPackageName; + this.appMainServiveName = appMainServiveName; + } + + public WinBoLLModel(String appPackageName, String appMainServiveName) { + this.appPackageName = appPackageName; + this.appMainServiveName = appMainServiveName; + } + + public void setAppPackageName(String appPackageName) { + this.appPackageName = appPackageName; + } + + public String getAppPackageName() { + return appPackageName; + } + + public void setAppMainServiveName(String appMainServiveName) { + this.appMainServiveName = appMainServiveName; + } + + public String getAppMainServiveName() { + return appMainServiveName; + } + + @Override + public String getName() { + return APPModel.class.getName(); + } + + @Override + public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { + super.writeThisToJsonWriter(jsonWriter); + jsonWriter.name("appPackageName").value(getAppPackageName()); + jsonWriter.name("appMainServiveName").value(getAppMainServiveName()); + } + + @Override + public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException { + if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else { + if (name.equals("appPackageName")) { + setAppPackageName(jsonReader.nextString()); + } else if (name.equals("appMainServiveName")) { + setAppMainServiveName(jsonReader.nextString()); + } else { + return false; + } + } + return true; + } + + @Override + public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { + jsonReader.beginObject(); + while (jsonReader.hasNext()) { + String name = jsonReader.nextName(); + if (!initObjectsFromJsonReader(jsonReader, name)) { + jsonReader.skipValue(); + } + } + // 结束 JSON 对象 + jsonReader.endObject(); + return this; + } +} + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/models/WinBoLLNewsBean.java b/winboll/src/main/java/cc/winboll/studio/winboll/models/WinBoLLNewsBean.java new file mode 100644 index 0000000..e749196 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/models/WinBoLLNewsBean.java @@ -0,0 +1,71 @@ +package cc.winboll.studio.winboll.models; + +/** + * @Author ZhanGSKen + * @Date 2025/05/10 09:36 + * @Describe WinBoLL 应用消息数据模型 + */ +import android.util.JsonReader; +import android.util.JsonWriter; +import cc.winboll.studio.libappbase.BaseBean; +import java.io.IOException; + +public class WinBoLLNewsBean extends BaseBean { + + public static final String TAG = "WinBoLLNewsBean"; + + String message; + + public WinBoLLNewsBean() { + this.message = ""; + } + + public WinBoLLNewsBean(String message) { + this.message = message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + @Override + public String getName() { + return WinBoLLNewsBean.class.getName(); + } + + @Override + public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { + super.writeThisToJsonWriter(jsonWriter); + jsonWriter.name("message").value(getMessage()); + } + + @Override + public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException { + if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else { + if (name.equals("message")) { + setMessage(jsonReader.nextString()); + } else { + return false; + } + } + return true; + } + + @Override + public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { + jsonReader.beginObject(); + while (jsonReader.hasNext()) { + String name = jsonReader.nextName(); + if (!initObjectsFromJsonReader(jsonReader, name)) { + jsonReader.skipValue(); + } + } + // 结束 JSON 对象 + jsonReader.endObject(); + return this; + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/receivers/APPNewsWidgetClickListener.java b/winboll/src/main/java/cc/winboll/studio/winboll/receivers/APPNewsWidgetClickListener.java new file mode 100644 index 0000000..4e3681d --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/receivers/APPNewsWidgetClickListener.java @@ -0,0 +1,36 @@ +package cc.winboll.studio.winboll.receivers; + +/** + * @Author ZhanGSKen + * @Date 2025/03/24 07:11:44 + */ +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.winboll.widgets.APPNewsWidget; + +public class APPNewsWidgetClickListener extends BroadcastReceiver { + + public static final String TAG = "APPNewsWidgetClickListener"; + public static final String ACTION_PRE = APPNewsWidgetClickListener.class.getName() + ".ACTION_PRE"; + public static final String ACTION_NEXT = APPNewsWidgetClickListener.class.getName() + ".ACTION_NEXT"; + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action == null) { + LogUtils.d(TAG, String.format("action %s", action)); + return; + } + if (action.equals(ACTION_PRE)) { + LogUtils.d(TAG, "ACTION_PRE"); + APPNewsWidget.prePage(context); + } else if (action.equals(ACTION_NEXT)) { + LogUtils.d(TAG, "ACTION_NEXT"); + APPNewsWidget.nextPage(context); + } else { + LogUtils.d(TAG, String.format("action %s", action)); + } + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/receivers/MainReceiver.java b/winboll/src/main/java/cc/winboll/studio/winboll/receivers/MainReceiver.java new file mode 100644 index 0000000..a0b9edb --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/receivers/MainReceiver.java @@ -0,0 +1,117 @@ +package cc.winboll.studio.winboll.receivers; + +/** + * @Author ZhanGSKen + * @Date 2025/02/13 06:58:04 + * @Describe 主要广播接收器 + */ +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.ToastUtils; +import cc.winboll.studio.winboll.WinBoLL; +import cc.winboll.studio.winboll.models.WinBoLLModel; +import cc.winboll.studio.winboll.models.WinBoLLNewsBean; +import cc.winboll.studio.winboll.services.MainService; +import cc.winboll.studio.winboll.sos.SOS; +import cc.winboll.studio.winboll.sos.SOSObject; +import cc.winboll.studio.winboll.widgets.APPNewsWidget; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class MainReceiver extends BroadcastReceiver { + + public static final String TAG = "MainReceiver"; + + public static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED"; + + WeakReference mwrService; + + public MainReceiver(MainService service) { + mwrService = new WeakReference(service); + } + + @Override + public void onReceive(Context context, Intent intent) { + String szAction = intent.getAction(); + if (szAction.equals(ACTION_BOOT_COMPLETED)) { + ToastUtils.show("ACTION_BOOT_COMPLETED"); + } else if (szAction.equals(IWinBoLLActivity.ACTION_BIND)) { + LogUtils.d(TAG, "ACTION_BIND"); + LogUtils.d(TAG, String.format("context.getPackageName() %s", context.getPackageName())); + LogUtils.d(TAG, String.format("intent.getAction() %s", intent.getAction())); + String szWinBoLLModel = intent.getStringExtra(WinBoLL.EXTRA_WINBOLLMODEL); + LogUtils.d(TAG, String.format("szAPPModel %s", szWinBoLLModel)); + if (szWinBoLLModel != null && !szWinBoLLModel.equals("")) { + try { + WinBoLLModel bean = WinBoLLModel.parseStringToBean(szWinBoLLModel, WinBoLLModel.class); + if (bean != null) { + String szAppPackageName = bean.getAppPackageName(); + LogUtils.d(TAG, String.format("szAppPackageName %s", szAppPackageName)); + String szAppMainServiveName = bean.getAppMainServiveName(); + LogUtils.d(TAG, String.format("szAppMainServiveName %s", szAppMainServiveName)); + mwrService.get().bindWinBoLLModelConnection(bean); + } + } catch (IOException e) { + LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); + } + } + } else if (intent.getAction().equals(SOS.ACTION_SOS)) { + LogUtils.d(TAG, "ACTION_SOS"); +// String sos = intent.getStringExtra(SOS.EXTRA_OBJECT); +// LogUtils.d(TAG, String.format("SOS %s", sos)); +// if (sos != null && !sos.equals("")) { +// SOSObject bean = SOS.parseSOSObject(sos); +// if (bean != null) { +// String szObjectPackageName = bean.getObjectPackageName(); +// LogUtils.d(TAG, String.format("szObjectPackageName %s", szObjectPackageName)); +// String szObjectServiveName = bean.getObjectServiveName(); +// LogUtils.d(TAG, String.format("szObjectServiveName %s", szObjectServiveName)); +// +// Intent intentService = new Intent(); +// intentService.setComponent(new ComponentName(szObjectPackageName, szObjectServiveName)); +// context.startService(intentService); +// +// String appName = AppUtils.getAppNameByPackageName(context, szObjectPackageName); +// LogUtils.d(TAG, String.format("appName %s", appName)); +// WinBoLLNewsBean appWinBoLLNewsBean = new WinBoLLNewsBean(appName); +// SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); +// String currentTime = sdf.format(new Date()); +// StringBuilder sbLine = new StringBuilder(); +// sbLine.append("["); +// sbLine.append(currentTime); +// sbLine.append("] Power to "); +// sbLine.append(appName); +// appWinBoLLNewsBean.setMessage(sbLine.toString()); +// +// APPNewsWidget.addWinBoLLNewsBean(context, appWinBoLLNewsBean); +// +// Intent intentWidget = new Intent(context, APPNewsWidget.class); +// intentWidget.setAction(APPNewsWidget.ACTION_RELOAD_REPORT); +// context.sendBroadcast(intentWidget); +// } +// +// +// } + } else { + ToastUtils.show(szAction); + } + } + + // 注册 Receiver + // + public void registerAction(MainService service) { + IntentFilter filter=new IntentFilter(); + filter.addAction(ACTION_BOOT_COMPLETED); + filter.addAction(SOS.ACTION_SOS); + filter.addAction(WinBoLL.ACTION_BIND); + //filter.addAction(Intent.ACTION_BATTERY_CHANGED); + service.registerReceiver(this, filter); + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/receivers/MyBroadcastReceiver.java b/winboll/src/main/java/cc/winboll/studio/winboll/receivers/MyBroadcastReceiver.java new file mode 100644 index 0000000..974ddfd --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/receivers/MyBroadcastReceiver.java @@ -0,0 +1,29 @@ +package cc.winboll.studio.winboll.receivers; + +/** + * @Author ZhanGSKen + * @Date 2025/02/13 21:19:09 + * @Describe MyBroadcastReceiver + */ +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.R; + +public class MyBroadcastReceiver extends BroadcastReceiver { + + public static final String TAG = "MyBroadcastReceiver"; + + @Override + public void onReceive(Context context, Intent intent) { +// if (context.getString(R.string.action_sos).equals(intent.getAction())) { +// String message = intent.getStringExtra("message"); +// String sosPackage = intent.getStringExtra("sosPackage"); +// +// // 处理接收到的广播消息 +// LogUtils.d(TAG, String.format("MyBroadcastReceiver action %s \n%s\n%s", intent.getAction(), sosPackage, message)); +// } + } +} + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/services/AssistantService.java b/winboll/src/main/java/cc/winboll/studio/winboll/services/AssistantService.java new file mode 100644 index 0000000..2fd6076 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/services/AssistantService.java @@ -0,0 +1,136 @@ +package cc.winboll.studio.winboll.services; + +/** + * @Author ZhanGSKen + * @Date 2025/02/14 03:38:31 + * @Describe 守护进程服务 + */ +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import cc.winboll.studio.libappbase.LogUtils; +import android.os.Binder; +import cc.winboll.studio.winboll.models.MainServiceBean; + +public class AssistantService extends Service { + + public static final String TAG = "AssistantService"; + + MainServiceBean mMainServiceBean; + MyServiceConnection mMyServiceConnection; + MainService mMainService; + boolean isBound = false; + volatile boolean isThreadAlive = false; + + public synchronized void setIsThreadAlive(boolean isThreadAlive) { + LogUtils.d(TAG, "setIsThreadAlive(...)"); + LogUtils.d(TAG, String.format("isThreadAlive %s", isThreadAlive)); + this.isThreadAlive = isThreadAlive; + } + + public boolean isThreadAlive() { + return isThreadAlive; + } + + @Override + public IBinder onBind(Intent intent) { + return new MyBinder(); + } + + @Override + public void onCreate() { + LogUtils.d(TAG, "onCreate"); + super.onCreate(); + + //mMyBinder = new MyBinder(); + if (mMyServiceConnection == null) { + mMyServiceConnection = new MyServiceConnection(); + } + // 设置运行参数 + setIsThreadAlive(false); + assistantService(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + LogUtils.d(TAG, "call onStartCommand(...)"); + assistantService(); + return START_STICKY; + } + + @Override + public void onDestroy() { + //LogUtils.d(TAG, "onDestroy"); + setIsThreadAlive(false); + // 解除绑定 + if (isBound) { + unbindService(mMyServiceConnection); + isBound = false; + } + super.onDestroy(); + } + + // 运行服务内容 + // + void assistantService() { + LogUtils.d(TAG, "assistantService()"); + mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class); + LogUtils.d(TAG, String.format("mMainServiceBean.isEnable() %s", mMainServiceBean.isEnable())); + if (mMainServiceBean.isEnable()) { + LogUtils.d(TAG, String.format("mIsThreadAlive %s", isThreadAlive())); + if (isThreadAlive() == false) { + // 设置运行状态 + setIsThreadAlive(true); + // 唤醒和绑定主进程 + wakeupAndBindMain(); + } + } + } + + // 唤醒和绑定主进程 + // + void wakeupAndBindMain() { + LogUtils.d(TAG, "wakeupAndBindMain()"); + // 绑定服务的Intent + Intent intent = new Intent(this, MainService.class); + startService(new Intent(this, MainService.class)); + bindService(intent, mMyServiceConnection, Context.BIND_IMPORTANT); + +// startService(new Intent(this, MainService.class)); +// bindService(new Intent(AssistantService.this, MainService.class), mMyServiceConnection, Context.BIND_IMPORTANT); + } + + // 主进程与守护进程连接时需要用到此类 + // + class MyServiceConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + LogUtils.d(TAG, "onServiceConnected(...)"); + MainService.MyBinder binder = (MainService.MyBinder) service; + mMainService = binder.getService(); + isBound = true; + } + + @Override + public void onServiceDisconnected(ComponentName name) { + LogUtils.d(TAG, "onServiceDisconnected(...)"); + mMainServiceBean = MainServiceBean.loadBean(AssistantService.this, MainServiceBean.class); + if (mMainServiceBean.isEnable()) { + wakeupAndBindMain(); + } + isBound = false; + mMainService = null; + } + } + + // 用于返回服务实例的Binder + public class MyBinder extends Binder { + AssistantService getService() { + LogUtils.d(TAG, "AssistantService MyBinder getService()"); + return AssistantService.this; + } + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/services/MainService.java b/winboll/src/main/java/cc/winboll/studio/winboll/services/MainService.java new file mode 100644 index 0000000..d287377 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/services/MainService.java @@ -0,0 +1,316 @@ +package cc.winboll.studio.winboll.services; + +/** + * @Author ZhanGSKen + * @Date 2025/02/13 06:56:41 + * @Describe 拨号主服务 + * 参考: + * 进程保活-双进程守护的正确姿势 + * https://blog.csdn.net/sinat_35159441/article/details/75267380 + * Android Service之onStartCommand方法研究 + * https://blog.csdn.net/cyp331203/article/details/38920491 + */ +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Binder; +import android.os.IBinder; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.winboll.MyTileService; +import cc.winboll.studio.winboll.handlers.MainServiceHandler; +import cc.winboll.studio.winboll.models.MainServiceBean; +import cc.winboll.studio.winboll.models.WinBoLLModel; +import cc.winboll.studio.winboll.receivers.MainReceiver; +import cc.winboll.studio.winboll.services.AssistantService; +import cc.winboll.studio.winboll.threads.MainServiceThread; +import cc.winboll.studio.winboll.widgets.APPNewsWidget; +import java.util.ArrayList; + +public class MainService extends Service { + + public static final String TAG = "MainService"; + + public static final int MSG_UPDATE_STATUS = 0; + + static MainService _mControlCenterService; + + volatile boolean isServiceRunning; + + MainServiceBean mMainServiceBean; + MainServiceThread mMainServiceThread; + MainServiceHandler mMainServiceHandler; + MyServiceConnection mMyServiceConnection; + AssistantService mAssistantService; + boolean isBound = false; + MainReceiver mMainReceiver; + ArrayList mAPPModelConnectionList; + + @Override + public IBinder onBind(Intent intent) { + return new MyBinder(); + } + + public MainServiceThread getRemindThread() { + return mMainServiceThread; + } + + @Override + public void onCreate() { + super.onCreate(); + LogUtils.d(TAG, "onCreate()"); + mAPPModelConnectionList = new ArrayList(); + + _mControlCenterService = MainService.this; + isServiceRunning = false; + mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class); + + if (mMyServiceConnection == null) { + mMyServiceConnection = new MyServiceConnection(); + } + mMainServiceHandler = new MainServiceHandler(this); + + // 运行服务内容 + mainService(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + LogUtils.d(TAG, "onStartCommand(...)"); + // 运行服务内容 + mainService(); + return (mMainServiceBean.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId); + } + + // 运行服务内容 + // + void mainService() { + LogUtils.d(TAG, "mainService()"); + mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class); + if (mMainServiceBean.isEnable() && isServiceRunning == false) { + LogUtils.d(TAG, "mainService() start running"); + isServiceRunning = true; + // 唤醒守护进程 + wakeupAndBindAssistant(); + + if (mMainReceiver == null) { + // 注册广播接收器 + mMainReceiver = new MainReceiver(this); + mMainReceiver.registerAction(this); + } + + // 启动小部件 + Intent intentTimeWidget = new Intent(this, APPNewsWidget.class); + intentTimeWidget.setAction(APPNewsWidget.ACTION_RELOAD_REPORT); + this.sendBroadcast(intentTimeWidget); + + startMainServiceThread(); + + MyTileService.updateServiceIconStatus(this); + + LogUtils.i(TAG, "Main Service Is Start."); + } + } + + // 唤醒和绑定守护进程 + // + void wakeupAndBindAssistant() { + LogUtils.d(TAG, "wakeupAndBindAssistant()"); + + Intent intent = new Intent(this, AssistantService.class); + startService(intent); + // 绑定服务的Intent + bindService(intent, mMyServiceConnection, Context.BIND_IMPORTANT); + } + + // 开启提醒铃声线程 + // + public void startMainServiceThread() { + LogUtils.d(TAG, "startMainServiceThread"); + if (mMainServiceThread == null) { + mMainServiceThread = new MainServiceThread(this, mMainServiceHandler); + LogUtils.d(TAG, "new MainServiceThread"); + } else { + if (mMainServiceThread.isExist() == true) { + mMainServiceThread = new MainServiceThread(this, mMainServiceHandler); + LogUtils.d(TAG, "renew MainServiceThread"); + } else { + // 提醒进程正在进行中就更新状态后退出 + LogUtils.d(TAG, "A mMainServiceThread running."); + return; + } + } + mMainServiceThread.start(); + } + + public void stopRemindThread() { + if (mMainServiceThread != null) { + mMainServiceThread.setIsExist(true); + mMainServiceThread = null; + } + } + + @Override + public void onDestroy() { + //LogUtils.d(TAG, "onDestroy"); + mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class); + if (mMainServiceBean.isEnable() == false) { + // 设置运行状态 + isServiceRunning = false;// 解除绑定 + if (isBound) { + unbindService(mMyServiceConnection); + isBound = false; + } + // 停止守护进程 + Intent intent = new Intent(this, AssistantService.class); + stopService(intent); + // 停止Receiver + if (mMainReceiver != null) { + unregisterReceiver(mMainReceiver); + mMainReceiver = null; + } + // 停止前台通知栏 + stopForeground(true); + // 停止消息提醒进程 + stopRemindThread(); + + MyTileService.updateServiceIconStatus(this); + + super.onDestroy(); + //LogUtils.d(TAG, "onDestroy done"); + } + } + + public void bindWinBoLLModelConnection(WinBoLLModel bean) { + LogUtils.d(TAG, "bindAPPModelConnection(...)"); + // 清理旧的绑定链接 + for (int i = mAPPModelConnectionList.size() - 1; i > -1; i--) { + APPConnection item = mAPPModelConnectionList.get(i); + if (item.isBindToAPP(bean)) { + LogUtils.d(TAG, "Bind Servive exist."); + unbindService(item); + mAPPModelConnectionList.remove(i); + } + } + + // 绑定服务 + APPConnection appConnection = new APPConnection(); + Intent intentService = new Intent(); + intentService.setComponent(new ComponentName(bean.getAppPackageName(), bean.getAppMainServiveName())); + bindService(intentService, appConnection, Context.BIND_IMPORTANT); + mAPPModelConnectionList.add(appConnection); + + Intent intentWidget = new Intent(this, APPNewsWidget.class); + intentWidget.setAction(APPNewsWidget.ACTION_WAKEUP_SERVICE); + WinBoLLModel appSOSBean = new WinBoLLModel(bean.getAppPackageName(), bean.getAppMainServiveName()); + intentWidget.putExtra("APPSOSBean", appSOSBean.toString()); + sendBroadcast(intentWidget); + } + + public class APPConnection implements ServiceConnection { + + ComponentName mComponentName; + + boolean isBindToAPP(WinBoLLModel bean) { + return mComponentName != null + && mComponentName.getClassName().equals(bean.getAppMainServiveName()) + && mComponentName.getPackageName().equals(bean.getAppPackageName()); + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + LogUtils.d(TAG, "onServiceConnected(...)"); + mComponentName = name; + LogUtils.d(TAG, String.format("onServiceConnected : \ngetClassName %s\ngetPackageName %s", name.getClassName(), name.getPackageName())); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + LogUtils.d(TAG, "onServiceDisconnected(...)"); + LogUtils.d(TAG, String.format("onServiceDisconnected : \ngetClassName %s\ngetPackageName %s", name.getClassName(), name.getPackageName())); + + // 尝试无参数启动一下服务 + String appPackage = mComponentName.getPackageName(); + LogUtils.d(TAG, String.format("appPackage %s", appPackage)); + String appMainServiceClassName = mComponentName.getClassName(); + LogUtils.d(TAG, String.format("appMainServiceClassName %s", appMainServiceClassName)); + + Intent intentService = new Intent(); + intentService.setComponent(new ComponentName(appPackage, appMainServiceClassName)); + startService(intentService); + } + + } + + // 主进程与守护进程连接时需要用到此类 + // + private class MyServiceConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + LogUtils.d(TAG, "onServiceConnected(...)"); + AssistantService.MyBinder binder = (AssistantService.MyBinder) service; + mAssistantService = binder.getService(); + isBound = true; + } + + @Override + public void onServiceDisconnected(ComponentName name) { + LogUtils.d(TAG, "onServiceDisconnected(...)"); + + if (mMainServiceBean.isEnable()) { + // 唤醒守护进程 + wakeupAndBindAssistant(); + } + isBound = false; + mAssistantService = null; + } + + } + + + // 用于返回服务实例的Binder + public class MyBinder extends Binder { + MainService getService() { + LogUtils.d(TAG, "MainService MyBinder getService()"); + return MainService.this; + } + } + +// // +// // 启动服务 +// // +// public static void startControlCenterService(Context context) { +// Intent intent = new Intent(context, MainService.class); +// context.startForegroundService(intent); +// } +// +// // +// // 停止服务 +// // +// public static void stopControlCenterService(Context context) { +// Intent intent = new Intent(context, MainService.class); +// context.stopService(intent); +// } + + public void appenMessage(String message) { + LogUtils.d(TAG, String.format("Message : %s", message)); + } + + public static void stopMainService(Context context) { + LogUtils.d(TAG, "stopMainService"); + MainServiceBean bean = new MainServiceBean(); + bean.setIsEnable(false); + MainServiceBean.saveBean(context, bean); + context.stopService(new Intent(context, MainService.class)); + } + + public static void startMainService(Context context) { + LogUtils.d(TAG, "startMainService"); + MainServiceBean bean = new MainServiceBean(); + bean.setIsEnable(true); + MainServiceBean.saveBean(context, bean); + context.startService(new Intent(context, MainService.class)); + } +} + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/services/TestDemoBindService.java b/winboll/src/main/java/cc/winboll/studio/winboll/services/TestDemoBindService.java new file mode 100644 index 0000000..8896879 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/services/TestDemoBindService.java @@ -0,0 +1,179 @@ +package cc.winboll.studio.winboll.services; + +/** + * @Author ZhanGSKen + * @Date 2025/03/07 12:45:49 + * @Describe 启动时申请绑定到APPBase主服务的服务示例 + */ +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.winboll.App; +import cc.winboll.studio.winboll.WinBoLL; +import cc.winboll.studio.winboll.models.TestDemoBindServiceBean; +import cc.winboll.studio.winboll.services.TestDemoBindService; +import cc.winboll.studio.winboll.sos.SOS; + +public class TestDemoBindService extends Service { + + public static final String TAG = "TestDemoBindService"; + + public static final String ACTION_ENABLE = TestDemoBindService.class.getName() + ".ACTION_ENABLE"; + public static final String ACTION_DISABLE = TestDemoBindService.class.getName() + ".ACTION_DISABLE"; + + volatile static TestThread _TestThread; + + volatile static boolean _IsRunning; + + public synchronized static void setIsRunning(boolean isRunning) { + _IsRunning = isRunning; + } + + public static boolean isRunning() { + return _IsRunning; + } + + @Override + public IBinder onBind(Intent intent) { + return new MyBinder(); + } + + public class MyBinder extends Binder { + public TestDemoBindService getService() { + return TestDemoBindService.this; + } + } + + @Override + public void onCreate() { + super.onCreate(); + LogUtils.d(TAG, "onCreate()"); + + run(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + LogUtils.d(TAG, "onStartCommand(...)"); + TestDemoBindServiceBean bean = TestDemoBindServiceBean.loadBean(this, TestDemoBindServiceBean.class); + if (bean == null) { + bean = new TestDemoBindServiceBean(); + } + + if (intent.getAction() != null) { + if (intent.getAction().equals(ACTION_ENABLE)) { + bean.setIsEnable(true); + LogUtils.d(TAG, "setIsEnable(true);"); + TestDemoBindServiceBean.saveBean(this, bean); + } else if (intent.getAction().equals(ACTION_DISABLE)) { + bean.setIsEnable(false); + LogUtils.d(TAG, "setIsEnable(false);"); + TestDemoBindServiceBean.saveBean(this, bean); + } + } + + run(); + + return (bean.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId); + //return super.onStartCommand(intent, flags, startId); + } + + void run() { + LogUtils.d(TAG, "run()"); + TestDemoBindServiceBean bean = TestDemoBindServiceBean.loadBean(this, TestDemoBindServiceBean.class); + if (bean == null) { + bean = new TestDemoBindServiceBean(); + TestDemoBindServiceBean.saveBean(this, bean); + } + if (bean.isEnable()) { + LogUtils.d(TAG, "run() bean.isEnable()"); + TestThread.getInstance(this).start(); + LogUtils.d(TAG, "_TestThread.start()"); + } + } + + + @Override + public void onDestroy() { + super.onDestroy(); + LogUtils.d(TAG, "onDestroy()"); + TestDemoBindServiceBean bean = TestDemoBindServiceBean.loadBean(this, TestDemoBindServiceBean.class); + if (bean == null) { + bean = new TestDemoBindServiceBean(); + } + + TestThread.getInstance(this).setIsExit(true); + + // 预防 APPBase 应用重启绑定失效。 + // 所以退出时检查本服务是否配置启用,如果启用就发送一个 SOS 信号。 + // 这样 APPBase 就会用组件方式启动本服务。 + if (bean.isEnable()) { + if (App.isDebugging()) { + SOS.sosToAppBaseBeta(this, TestDemoBindService.class.getName()); + } else { + SOS.sosToAppBase(this, TestDemoBindService.class.getName()); + } + } + + _IsRunning = false; + } + + static class TestThread extends Thread { + + volatile static TestThread _TestThread; + Context mContext; + volatile boolean isStarted = false; + volatile boolean isExit = false; + + TestThread(Context context) { + super(); + mContext = context; + } + + public static synchronized TestThread getInstance(Context context) { + if (_TestThread != null) { + _TestThread.setIsExit(true); + } + _TestThread = new TestThread(context); + + return _TestThread; + } + + public synchronized void setIsExit(boolean isExit) { + this.isExit = isExit; + } + + public boolean isExit() { + return isExit; + } + + @Override + public void run() { + if (isStarted == false) { + isStarted = true; + super.run(); + LogUtils.d(TAG, "run() start"); + if (App.isDebugging()) { + WinBoLL.bindToAPPBaseBeta(mContext, TestDemoBindService.class.getName()); + } else { + WinBoLL.bindToAPPBase(mContext, TestDemoBindService.class.getName()); + } + + while (!isExit()) { + LogUtils.d(TAG, "run()"); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); + } + } + + LogUtils.d(TAG, "run() exit"); + } + } + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/services/TestDemoService.java b/winboll/src/main/java/cc/winboll/studio/winboll/services/TestDemoService.java new file mode 100644 index 0000000..7364b9b --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/services/TestDemoService.java @@ -0,0 +1,156 @@ +package cc.winboll.studio.winboll.services; + +/** + * @Author ZhanGSKen + * @Date 2025/03/07 12:39:24 + * @Describe 普通服务示例 + */ +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; +import cc.winboll.studio.winboll.models.TestDemoServiceBean; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.winboll.models.TestDemoServiceBean; + +public class TestDemoService extends Service { + + public static final String TAG = "TestDemoService"; + + public static final String ACTION_ENABLE = TestDemoService.class.getName() + ".ACTION_ENABLE"; + public static final String ACTION_DISABLE = TestDemoService.class.getName() + ".ACTION_DISABLE"; + + volatile static TestThread _TestThread; + + volatile static boolean _IsRunning; + + public synchronized static void setIsRunning(boolean isRunning) { + _IsRunning = isRunning; + } + + public static boolean isRunning() { + return _IsRunning; + } + + @Override + public IBinder onBind(Intent intent) { + return new MyBinder(); + } + + public class MyBinder extends Binder { + public TestDemoService getService() { + return TestDemoService.this; + } + } + + @Override + public void onCreate() { + super.onCreate(); + LogUtils.d(TAG, "onCreate()"); + + + run(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + LogUtils.d(TAG, "onStartCommand(...)"); + TestDemoServiceBean bean = TestDemoServiceBean.loadBean(this, TestDemoServiceBean.class); + if (bean == null) { + bean = new TestDemoServiceBean(); + } + + if (intent.getAction() != null) { + if (intent.getAction().equals(ACTION_ENABLE)) { + bean.setIsEnable(true); + LogUtils.d(TAG, "setIsEnable(true);"); + TestDemoServiceBean.saveBean(this, bean); + } else if (intent.getAction().equals(ACTION_DISABLE)) { + bean.setIsEnable(false); + LogUtils.d(TAG, "setIsEnable(false);"); + TestDemoServiceBean.saveBean(this, bean); + } + } + + run(); + + return (bean.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId); + //return super.onStartCommand(intent, flags, startId); + } + + void run() { + LogUtils.d(TAG, "run()"); + TestDemoServiceBean bean = TestDemoServiceBean.loadBean(this, TestDemoServiceBean.class); + if (bean == null) { + bean = new TestDemoServiceBean(); + TestDemoServiceBean.saveBean(this, bean); + } + if (bean.isEnable()) { + LogUtils.d(TAG, "run() bean.isEnable()"); + TestThread.getInstance(this).start(); + LogUtils.d(TAG, "_TestThread.start()"); + } + } + + + @Override + public void onDestroy() { + super.onDestroy(); + LogUtils.d(TAG, "onDestroy()"); + TestThread.getInstance(this).setIsExit(true); + + _IsRunning = false; + } + + static class TestThread extends Thread { + + volatile static TestThread _TestThread; + Context mContext; + volatile boolean isStarted = false; + volatile boolean isExit = false; + + TestThread(Context context) { + super(); + mContext = context; + } + + public static synchronized TestThread getInstance(Context context) { + if (_TestThread != null) { + _TestThread.setIsExit(true); + } + _TestThread = new TestThread(context); + + return _TestThread; + } + + public synchronized void setIsExit(boolean isExit) { + this.isExit = isExit; + } + + public boolean isExit() { + return isExit; + } + + @Override + public void run() { + if (isStarted == false) { + isStarted = true; + super.run(); + LogUtils.d(TAG, "run() start"); + + while (!isExit()) { + LogUtils.d(TAG, "run()"); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); + } + } + + LogUtils.d(TAG, "run() exit"); + } + } + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOS.java b/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOS.java new file mode 100644 index 0000000..c45d8eb --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOS.java @@ -0,0 +1,59 @@ +package cc.winboll.studio.winboll.sos; + +/** + * @Author ZhanGSKen + * @Date 2025/03/02 09:36:29 + * @Describe WinBoLL 应用 SOS 机理保护类 + */ +import android.content.Context; +import android.content.Intent; +import cc.winboll.studio.libappbase.LogUtils; +import java.io.IOException; + +public class SOS { + + public static final String TAG = "SOS"; + + public static final String ACTION_SOS = SOS.class.getName() + ".ACTION_SOS"; + public static final String EXTRA_OBJECT = "EXTRA_OBJECT"; + + public static void sosToAppBase(Context context, String sosService) { + LogUtils.d(TAG, "sosToAppBase()"); + String szToPackage = "cc.winboll.studio.appbase"; + sos(context, szToPackage, sosService); + + } + + public static void sosToAppBaseBeta(Context context, String sosService) { + LogUtils.d(TAG, "sosToAppBaseBeta()"); + String szToPackage = "cc.winboll.studio.appbase.beta"; + sos(context, szToPackage, sosService); + + } + + static void sos(Context context, String szToPackage, String sosService) { + LogUtils.d(TAG, "sos(...)"); + Intent intent = new Intent(ACTION_SOS); + intent.putExtra(EXTRA_OBJECT, genSOSObject(context.getPackageName(), sosService)); + intent.setPackage(szToPackage); + LogUtils.d(TAG, String.format("ACTION_SOS :\nTo Package : %sSOS Service : %s\n", szToPackage, sosService)); + context.sendBroadcast(intent); + } + + public static SOSObject parseSOSObject(String szSOSObject) { + try { + return SOSObject.parseStringToBean(szSOSObject, SOSObject.class); + } catch (IOException e) { + LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); + } + return null; + } + + public static String sosObjectToString(SOSObject object) { + return object.toString(); + } + + public static String genSOSObject(String objectPackageName, String objectServiveName) { + return (new SOSObject(objectPackageName, objectServiveName)).toString(); + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSCenterService.java b/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSCenterService.java new file mode 100644 index 0000000..039d8e6 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSCenterService.java @@ -0,0 +1,182 @@ +package cc.winboll.studio.winboll.sos; + +/** + * @Author ZhanGSKen + * @Date 2025/02/27 14:00:21 + * @Describe Simple Operate Signal Service Center. + * 简单操作信号服务中心 + */ +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.os.IInterface; +import android.os.Parcel; +import android.os.RemoteException; +import java.io.FileDescriptor; +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import cc.winboll.studio.libappbase.LogUtils; + +public class SOSCenterService extends Service { + + public static final String TAG = "SOSCenterService"; + + private final IBinder binder =(IBinder)new SOSBinder(); + + SOSCenterServiceModel mSOSCenterServiceModel; + static MainThread _MainThread; + public static synchronized MainThread getMainThreadInstance() { + if (_MainThread == null) { + _MainThread = new MainThread(); + } + return _MainThread; + } + + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + public class SOSBinder implements IBinder { + + @Override + public void dump(FileDescriptor fileDescriptor, String[] string) throws RemoteException { + } + + @Override + public void dumpAsync(FileDescriptor fileDescriptor, String[] string) throws RemoteException { + } + + @Override + public String getInterfaceDescriptor() throws RemoteException { + return null; + } + + @Override + public boolean isBinderAlive() { + return false; + } + + @Override + public void linkToDeath(IBinder.DeathRecipient deathRecipient, int p) throws RemoteException { + } + + @Override + public boolean pingBinder() { + return false; + } + + @Override + public IInterface queryLocalInterface(String string) { + return null; + } + + @Override + public boolean transact(int p, Parcel parcel, Parcel parcel1, int p1) throws RemoteException { + return false; + } + + @Override + public boolean unlinkToDeath(IBinder.DeathRecipient deathRecipient, int p) { + return false; + } + + public static final String TAG = "SOSBinder"; + SOSCenterService getService() { + return SOSCenterService.this; + } + } + + @Override + public void onCreate() { + super.onCreate(); + LogUtils.d(TAG, "onCreate"); + mSOSCenterServiceModel = SOSCenterServiceModel.loadBean(this, SOSCenterServiceModel.class); + if(mSOSCenterServiceModel == null) { + mSOSCenterServiceModel = new SOSCenterServiceModel(); + SOSCenterServiceModel.saveBean(this, mSOSCenterServiceModel); + } + runMainThread(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + LogUtils.d(TAG, "onStartCommand"); + + runMainThread(); + + return mSOSCenterServiceModel.isEnable() ? Service.START_STICKY: super.onStartCommand(intent, flags, startId); + } + + void runMainThread() { + mSOSCenterServiceModel = mSOSCenterServiceModel.loadBean(this, SOSCenterServiceModel.class); + if (mSOSCenterServiceModel.isEnable() + && _MainThread == null) { + getMainThreadInstance().start(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + LogUtils.d(TAG, "onDestroy"); + mSOSCenterServiceModel = SOSCenterServiceModel.loadBean(this, SOSCenterServiceModel.class); + if (mSOSCenterServiceModel.isEnable()) { + LogUtils.d(TAG, "mSOSCenterServiceModel.isEnable()"); +// ISOSAPP iSOSAPP = (ISOSAPP)getApplication(); +// iSOSAPP.helpISOSService(getISOSServiceIntentWhichAskForHelp()); + } + if (_MainThread != null) { + _MainThread.isExist = true; + _MainThread = null; + } + } + + public static void stopISOSService(Context context) { + LogUtils.d(TAG, "stopISOSService"); + SOSCenterServiceModel bean = new SOSCenterServiceModel(); + bean.setIsEnable(false); + SOSCenterServiceModel.saveBean(context, bean); + context.stopService(new Intent(context, SOSCenterServiceModel.class)); + } + + public static void startISOSService(Context context) { + LogUtils.d(TAG, "startISOSService"); + SOSCenterServiceModel bean = new SOSCenterServiceModel(); + bean.setIsEnable(true); + SOSCenterServiceModel.saveBean(context, bean); + context.startService(new Intent(context, SOSCenterServiceModel.class)); + } + + public String getMessage() { + return "Hello from SOSCenterServiceModel"; + } + + static class MainThread extends Thread { + volatile boolean isExist = false; + + public void setIsExist(boolean isExist) { + this.isExist = isExist; + } + + public boolean isExist() { + return isExist; + } + + @Override + public void run() { + super.run(); + while (!isExist) { + LogUtils.d(TAG, "run"); + try { + sleep(1000); + } catch (InterruptedException e) { + LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); + } + } + } + + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSCenterServiceModel.java b/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSCenterServiceModel.java new file mode 100644 index 0000000..3f226ae --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSCenterServiceModel.java @@ -0,0 +1,69 @@ +package cc.winboll.studio.winboll.sos; + +/** + * @Author ZhanGSKen + * @Date 2025/03/02 09:49:45 + * @Describe SOSCenterServiceModel + * Simple Operate Signal Service Model. + */ +import android.util.JsonReader; +import android.util.JsonWriter; +import cc.winboll.studio.libappbase.BaseBean; +import java.io.IOException; + +public class SOSCenterServiceModel extends BaseBean { + + public static final String TAG = "SOSCenterServiceModel"; + + boolean isEnable; + + public SOSCenterServiceModel() { + this.isEnable = false; + } + + public void setIsEnable(boolean isEnable) { + this.isEnable = isEnable; + } + + public boolean isEnable() { + return isEnable; + } + + @Override + public String getName() { + return SOSCenterServiceModel.class.getName(); + } + + @Override + public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { + super.writeThisToJsonWriter(jsonWriter); + jsonWriter.name("isEnable").value(isEnable()); + + } + + @Override + public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException { + if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else { + if (name.equals("isEnable")) { + setIsEnable(jsonReader.nextBoolean()); + } else { + return false; + } + } + return true; + } + + @Override + public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { + jsonReader.beginObject(); + while (jsonReader.hasNext()) { + String name = jsonReader.nextName(); + if (!initObjectsFromJsonReader(jsonReader, name)) { + jsonReader.skipValue(); + } + } + // 结束 JSON 对象 + jsonReader.endObject(); + return this; + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSCenterServiceReceiver.java b/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSCenterServiceReceiver.java new file mode 100644 index 0000000..14dd36a --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSCenterServiceReceiver.java @@ -0,0 +1,29 @@ +package cc.winboll.studio.winboll.sos; + +/** + * @Author ZhanGSKen + * @Date 2025/02/27 14:04:35 + * @Describe SOSCenterServiceReceiver + */ +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import cc.winboll.studio.libappbase.LogUtils; + +public class SOSCenterServiceReceiver extends BroadcastReceiver { + + public static final String TAG = "SOSCenterServiceReceiver"; + + public static final String ACTION_SOS = SOSCenterServiceReceiver.class.getName() + ".ACTION_SOS"; + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(ACTION_SOS)) { + // 处理接收到的广播消息 + LogUtils.d(TAG, String.format("Action %s \n%s\n%s", action)); + } else { + LogUtils.d(TAG, String.format("%s", action)); + } + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSObject.java b/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSObject.java new file mode 100644 index 0000000..ec07b38 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSObject.java @@ -0,0 +1,86 @@ +package cc.winboll.studio.winboll.sos; + +/** + * @Author ZhanGSKen + * @Date 2025/02/27 14:12:05 + * @Describe SOSBean + */ +import android.util.JsonReader; +import android.util.JsonWriter; +import cc.winboll.studio.libappbase.BaseBean; +import java.io.IOException; + +public class SOSObject extends BaseBean { + + public static final String TAG = "SOSObject"; + + String objectPackageName; + String objectServiveName; + + public SOSObject() { + this.objectPackageName = ""; + this.objectServiveName = ""; + } + + public SOSObject(String objectPackageName, String objectServiveName) { + this.objectPackageName = objectPackageName; + this.objectServiveName = objectServiveName; + } + + public void setObjectPackageName(String objectPackageName) { + this.objectPackageName = objectPackageName; + } + + public String getObjectPackageName() { + return objectPackageName; + } + + public void setObjectServiveName(String objectServiveName) { + this.objectServiveName = objectServiveName; + } + + public String getObjectServiveName() { + return objectServiveName; + } + + @Override + public String getName() { + return SOSObject.class.getName(); + } + + @Override + public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { + super.writeThisToJsonWriter(jsonWriter); + jsonWriter.name("objectPackageName").value(getObjectPackageName()); + jsonWriter.name("objectServiveName").value(getObjectServiveName()); + + } + + @Override + public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException { + if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else { + if (name.equals("objectPackageName")) { + setObjectPackageName(jsonReader.nextString()); + } else if (name.equals("objectServiveName")) { + setObjectServiveName(jsonReader.nextString()); + } else { + return false; + } + } + return true; + } + + @Override + public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { + jsonReader.beginObject(); + while (jsonReader.hasNext()) { + String name = jsonReader.nextName(); + if (!initObjectsFromJsonReader(jsonReader, name)) { + jsonReader.skipValue(); + } + } + // 结束 JSON 对象 + jsonReader.endObject(); + return this; + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/threads/MainServiceThread.java b/winboll/src/main/java/cc/winboll/studio/winboll/threads/MainServiceThread.java new file mode 100644 index 0000000..80ecc3f --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/threads/MainServiceThread.java @@ -0,0 +1,54 @@ +package cc.winboll.studio.winboll.threads; + +/** + * @Author ZhanGSKen + * @Date 2025/02/14 03:46:44 + */ +import android.content.Context; +import cc.winboll.studio.winboll.handlers.MainServiceHandler; +import cc.winboll.studio.libappbase.LogUtils; +import java.lang.ref.WeakReference; + +public class MainServiceThread extends Thread { + + public static final String TAG = "MainServiceThread"; + + Context mContext; + + // 控制线程是否退出的标志 + volatile boolean isExist = false; + + // 服务Handler, 用于线程发送消息使用 + WeakReference mwrMainServiceHandler; + + public void setIsExist(boolean isExist) { + this.isExist = isExist; + } + + public boolean isExist() { + return isExist; + } + + public MainServiceThread(Context context, MainServiceHandler handler) { + mContext = context; + mwrMainServiceHandler = new WeakReference(handler); + } + + @Override + public void run() { + LogUtils.d(TAG, "run()"); + + while (!isExist()) { + //ToastUtils.show("run()"); + //LogUtils.d(TAG, "run()"); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); + } + } + LogUtils.d(TAG, "run() exit."); + } + +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/unittest/TestWeWorkSpecSDK.java b/winboll/src/main/java/cc/winboll/studio/winboll/unittest/TestWeWorkSpecSDK.java new file mode 100644 index 0000000..5fdac27 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/unittest/TestWeWorkSpecSDK.java @@ -0,0 +1,311 @@ +package cc.winboll.studio.winboll.unittest; + +import android.app.Activity; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.view.View; +import android.widget.Button; +import android.widget.Toast; +import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.winboll.R; +import cc.winboll.studio.winboll.activities.WinBoLLActivity; + +/** + * @Author 豆包&ZhanGSKen + * @Date 2026/01/03 10:52 + * @Describe 企业微信SDK接口测试(基础调试版) + * 包含:SDK初始化、基础接口调用、日志输出、主线程回调处理 + */ +public class TestWeWorkSpecSDK extends WinBoLLActivity implements IWinBoLLActivity, View.OnClickListener { + + public static final String TAG = "TestWeWorkSpecSDK"; + + // ------------------- 企业微信SDK配置常量(需替换为实际项目参数) ------------------- + // 企业微信 CorpID(从企业微信管理后台获取) + private static final String CORP_ID = "wwb37c73f34c722852"; + // 应用 AgentID(从企业微信应用管理后台获取) + private static final String AGENT_ID = "your_agent_id_here"; + // 应用 Secret(从企业微信应用管理后台获取,注意保密) + private static final String APP_SECRET = "your_app_secret_here"; + + // ------------------- Handler消息标识(主线程处理SDK回调) ------------------- + private static final int MSG_SDK_INIT_SUCCESS = 1001; + private static final int MSG_SDK_INIT_FAILED = 1002; + private static final int MSG_GET_CORP_INFO_SUCCESS = 1003; + private static final int MSG_GET_CORP_INFO_FAILED = 1004; + + // ------------------- 控件声明 ------------------- + private Button mBtnInitSDK; + private Button mBtnGetCorpInfo; + private Button mBtnCheckAuth; + + // ------------------- 主线程Handler(处理SDK异步回调) ------------------- + private Handler mWeWorkHandler; + + + @Override + public Activity getActivity() { + return this; + } + + @Override + public String getTag() { + return TAG; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_test_weworkspecsdk); + + // 初始化控件 + initViews(); + // 绑定点击事件 + initEvents(); + // 初始化Handler(主线程处理回调,更新UI) + initHandler(); + // 初始化SDK(可选:启动时自动初始化,或点击按钮初始化) + // initWeWorkSDK(); + } + + /** + * 初始化控件(Java 7 显式绑定) + */ + private void initViews() { + mBtnInitSDK = (Button) findViewById(R.id.btn_init_sdk); + mBtnGetCorpInfo = (Button) findViewById(R.id.btn_get_corp_info); + mBtnCheckAuth = (Button) findViewById(R.id.btn_check_auth); + } + + /** + * 绑定点击事件(Java 7 匿名内部类) + */ + private void initEvents() { + mBtnInitSDK.setOnClickListener(this); + mBtnGetCorpInfo.setOnClickListener(this); + mBtnCheckAuth.setOnClickListener(this); + } + + /** + * 初始化主线程Handler(处理SDK异步回调,安全更新UI) + */ + private void initHandler() { + mWeWorkHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + super.handleMessage(msg); + switch (msg.what) { + case MSG_SDK_INIT_SUCCESS: + showToast("企业微信SDK初始化成功"); + LogUtils.d(TAG, "SDK初始化成功"); + break; + case MSG_SDK_INIT_FAILED: + String initError = (String) msg.obj; + showToast("SDK初始化失败:" + initError); + LogUtils.e(TAG, "SDK初始化失败:" + initError); + break; + case MSG_GET_CORP_INFO_SUCCESS: + String corpInfo = (String) msg.obj; + showToast("获取企业信息成功"); + LogUtils.d(TAG, "企业信息:" + corpInfo); + break; + case MSG_GET_CORP_INFO_FAILED: + String corpError = (String) msg.obj; + showToast("获取企业信息失败:" + corpError); + LogUtils.e(TAG, "获取企业信息失败:" + corpError); + break; + default: + break; + } + } + }; + } + + // ------------------- 企业微信SDK核心接口调用 ------------------- + + /** + * 初始化企业微信SDK(异步操作,通过Handler回调结果) + */ + private void initWeWorkSDK() { + showToast("开始初始化企业微信SDK..."); + // 模拟SDK异步初始化(实际项目中替换为企业微信SDK的真实初始化接口) + new Thread(new Runnable() { + @Override + public void run() { + try { + // 真实SDK初始化逻辑示例: + // WeWorkSDK.init(TestWeWorkSpecSDK.this, CORP_ID, AGENT_ID, new WeWorkSDKCallback() { + // @Override + // public void onSuccess() { + // mWeWorkHandler.sendEmptyMessage(MSG_SDK_INIT_SUCCESS); + // } + // + // @Override + // public void onFailure(String errorMsg) { + // Message msg = Message.obtain(); + // msg.what = MSG_SDK_INIT_FAILED; + // msg.obj = errorMsg; + // mWeWorkHandler.sendMessage(msg); + // } + // }); + + // 调试模拟:休眠1秒,模拟异步初始化 + Thread.sleep(1000); + // 模拟初始化成功(如需测试失败,替换为发送MSG_SDK_INIT_FAILED) + mWeWorkHandler.sendEmptyMessage(MSG_SDK_INIT_SUCCESS); + // 模拟初始化失败 + // Message msg = Message.obtain(); + // msg.what = MSG_SDK_INIT_FAILED; + // msg.obj = "CorpID或AgentID错误"; + // mWeWorkHandler.sendMessage(msg); + } catch (InterruptedException e) { + e.printStackTrace(); + Message msg = Message.obtain(); + msg.what = MSG_SDK_INIT_FAILED; + msg.obj = "线程中断:" + e.getMessage(); + mWeWorkHandler.sendMessage(msg); + } + } + }).start(); + } + + /** + * 获取企业基本信息(异步操作,需先初始化SDK) + */ + private void getCorpInfo() { + if (!isSDKInitialized()) { + showToast("请先初始化SDK"); + return; + } + showToast("开始获取企业信息..."); + // 模拟SDK异步获取企业信息(实际项目中替换为真实接口) + new Thread(new Runnable() { + @Override + public void run() { + try { + // 真实SDK接口示例: + // WeWorkSDK.getCorpInfo(APP_SECRET, new CorpInfoCallback() { + // @Override + // public void onSuccess(CorpInfo info) { + // Message msg = Message.obtain(); + // msg.what = MSG_GET_CORP_INFO_SUCCESS; + // msg.obj = "企业名称:" + info.getCorpName() + ",企业ID:" + info.getCorpId(); + // mWeWorkHandler.sendMessage(msg); + // } + // + // @Override + // public void onFailure(String errorMsg) { + // Message msg = Message.obtain(); + // msg.what = MSG_GET_CORP_INFO_FAILED; + // msg.obj = errorMsg; + // mWeWorkHandler.sendMessage(msg); + // } + // }); + + // 调试模拟:休眠1秒,模拟异步获取 + Thread.sleep(1000); + // 模拟获取成功 + Message successMsg = Message.obtain(); + successMsg.what = MSG_GET_CORP_INFO_SUCCESS; + successMsg.obj = "企业名称:WinBoLL Studio,企业ID:" + CORP_ID; + mWeWorkHandler.sendMessage(successMsg); + // 模拟获取失败 + // Message failMsg = Message.obtain(); + // failMsg.what = MSG_GET_CORP_INFO_FAILED; + // failMsg.obj = "AppSecret错误或权限不足"; + // mWeWorkHandler.sendMessage(failMsg); + } catch (InterruptedException e) { + e.printStackTrace(); + Message msg = Message.obtain(); + msg.what = MSG_GET_CORP_INFO_FAILED; + msg.obj = "线程中断:" + e.getMessage(); + mWeWorkHandler.sendMessage(msg); + } + } + }).start(); + } + + /** + * 检查当前用户是否已授权(同步操作,示例) + */ + private void checkAuthStatus() { + if (!isSDKInitialized()) { + showToast("请先初始化SDK"); + return; + } + // 真实SDK接口示例: + // boolean isAuthorized = WeWorkSDK.isAuthorized(); + // 调试模拟:默认返回true + boolean isAuthorized = true; + + if (isAuthorized) { + showToast("用户已授权"); + LogUtils.d(TAG, "当前用户已授权企业微信应用"); + } else { + showToast("用户未授权,请先授权"); + LogUtils.d(TAG, "当前用户未授权企业微信应用"); + // 真实项目中可调用授权接口: + // WeWorkSDK.requestAuth(TestWeWorkSpecSDK.this, new AuthCallback() { + // @Override + // public void onSuccess(String code) { + // showToast("授权成功,code:" + code); + // } + // + // @Override + // public void onFailure(String errorMsg) { + // showToast("授权失败:" + errorMsg); + // } + // }); + } + } + + // ------------------- 工具方法 ------------------- + + /** + * 检查SDK是否已初始化(模拟方法,实际项目中替换为SDK的真实状态检查) + */ + private boolean isSDKInitialized() { + // 真实SDK可通过静态方法检查状态: + // return WeWorkSDK.isInitialized(); + // 调试模拟:假设Handler不为空即表示已初始化 + return mWeWorkHandler != null; + } + + /** + * 显示Toast提示(Java 7 简化封装) + */ + private void showToast(String msg) { + Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); + } + + // ------------------- 点击事件处理 ------------------- + + @Override + public void onClick(View v) { + int id = v.getId(); + if (id == R.id.btn_init_sdk) { + initWeWorkSDK(); + } else if (id == R.id.btn_get_corp_info) { + getCorpInfo(); + } else if (id == R.id.btn_check_auth) { + checkAuthStatus(); + } + } + + // ------------------- 生命周期管理 ------------------- + + @Override + protected void onDestroy() { + super.onDestroy(); + // 释放Handler资源,避免内存泄漏 + if (mWeWorkHandler != null) { + mWeWorkHandler.removeCallbacksAndMessages(null); + mWeWorkHandler = null; + } + // 真实SDK需调用销毁方法: + // WeWorkSDK.destroy(); + } +} + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/utils/APPPlusUtils.java b/winboll/src/main/java/cc/winboll/studio/winboll/utils/APPPlusUtils.java new file mode 100644 index 0000000..2004f93 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/utils/APPPlusUtils.java @@ -0,0 +1,163 @@ +package cc.winboll.studio.winboll.utils; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; +import android.widget.Toast; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.winboll.App; +import cc.winboll.studio.winboll.R; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/27 08:56 + * @Describe APPPlusUtils + */ +public class APPPlusUtils { + public static final String TAG = "APPPlusUtils"; + + // 快捷方式配置(名称+图标,需与实际资源匹配) +// private static final String PLUS_SHORTCUT_NAME = "位置服务-Laojun"; +// private static final int PLUS_SHORTCUT_ICON = R.mipmap.ic_launcher; // Laojun 图标资源 + + /** + * 添加Plus组件与图标 + */ + public static boolean switchAppLauncherToComponent(Context context, String componentName) { + if (context == null) { + LogUtils.d(TAG, "切换失败:上下文为空"); + Toast.makeText(context, context.getString(R.string.app_name) + "图标切换失败", Toast.LENGTH_SHORT).show(); + return false; + } + + PackageManager pm = context.getPackageManager(); + + ComponentName plusComponentSwitchTo = new ComponentName(context, componentName); + ComponentName plusComponentEN1 = new ComponentName(context, App.COMPONENT_EN1); + ComponentName plusComponentCN1 = new ComponentName(context, App.COMPONENT_CN1); + ComponentName plusComponentCN2 = new ComponentName(context, App.COMPONENT_CN2); + + try { + disableComponent(pm, plusComponentEN1); + disableComponent(pm, plusComponentCN1); + disableComponent(pm, plusComponentCN2); + enableComponent(pm, plusComponentSwitchTo); + + return true; + + } catch (Exception e) { + LogUtils.e(TAG, "图标切换失败:" + e.getMessage()); + Toast.makeText(context, context.getString(R.string.app_name) + "图标切换失败" + e.getMessage(), Toast.LENGTH_SHORT).show(); + return false; + } + } + + /** + * 创建指定组件的桌面快捷方式(自动去重,兼容 Android 8.0+) + * @param component 目标组件(如 LAOJUN_ACTIVITY) + * @param name 快捷方式名称 + * @param iconRes 快捷方式图标资源ID + * @return 是否创建成功 + */ + private static boolean createComponentShortcut(Context context, ComponentName component, String name, int iconRes) { + if (context == null || component == null || name == null || iconRes == 0) { + LogUtils.d(TAG, "快捷方式创建失败:参数为空"); + return false; + } + + // Android 8.0+(API 26+):使用 ShortcutManager(系统推荐) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + try { + PackageManager pm = context.getPackageManager(); + android.content.pm.ShortcutManager shortcutManager = context.getSystemService(android.content.pm.ShortcutManager.class); + if (shortcutManager == null || !shortcutManager.isRequestPinShortcutSupported()) { + LogUtils.d(TAG, "系统不支持创建快捷方式"); + return false; + } + + // 检查是否已存在该组件的快捷方式(去重) + for (android.content.pm.ShortcutInfo info : shortcutManager.getPinnedShortcuts()) { + if (component.getClassName().equals(info.getIntent().getComponent().getClassName())) { + LogUtils.d(TAG, "快捷方式已存在:" + component.getClassName()); + return true; + } + } + + // 构建启动目标组件的意图 + Intent launchIntent = new Intent(Intent.ACTION_MAIN) + .setComponent(component) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + // 构建快捷方式信息 + android.content.pm.ShortcutInfo shortcutInfo = new android.content.pm.ShortcutInfo.Builder(context, component.getClassName()) + .setShortLabel(name) + .setLongLabel(name) + .setIcon(android.graphics.drawable.Icon.createWithResource(context, iconRes)) + .setIntent(launchIntent) + .build(); + + // 请求创建快捷方式(需用户确认) + shortcutManager.requestPinShortcut(shortcutInfo, null); + return true; + + } catch (Exception e) { + LogUtils.d(TAG, "Android O+ 快捷方式创建失败:" + e.getMessage()); + return false; + } + } else { + // Android 8.0 以下:使用广播(兼容旧机型) + try { + // 构建启动目标组件的意图 + Intent launchIntent = new Intent(Intent.ACTION_MAIN) + .setComponent(component) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + // 构建创建快捷方式的广播意图 + Intent installIntent = new Intent("com.android.launcher.action.INSTALL_SHORTCUT"); + installIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, launchIntent); + installIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name); + installIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, + Intent.ShortcutIconResource.fromContext(context, iconRes)); + installIntent.putExtra("duplicate", false); // 禁止重复创建 + + context.sendBroadcast(installIntent); + return true; + + } catch (Exception e) { + LogUtils.d(TAG, "Android O- 快捷方式创建失败:" + e.getMessage()); + return false; + } + } + } + + /** + * 启用组件(带状态检查,避免重复操作) + */ + private static void enableComponent(PackageManager pm, ComponentName component) { + if (pm.getComponentEnabledSetting(component) != PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { + pm.setComponentEnabledSetting( + component, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP | PackageManager.SYNCHRONOUS + ); + } + } + + /** + * 禁用组件(带状态检查,避免重复操作) + */ + private static void disableComponent(PackageManager pm, ComponentName component) { + if (pm.getComponentEnabledSetting(component) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED) { + pm.setComponentEnabledSetting( + component, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP | PackageManager.SYNCHRONOUS + ); + } + } +} + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/utils/OkHttpUtil.java b/winboll/src/main/java/cc/winboll/studio/winboll/utils/OkHttpUtil.java new file mode 100644 index 0000000..4e23dcb --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/utils/OkHttpUtil.java @@ -0,0 +1,83 @@ +package cc.winboll.studio.winboll.utils; + +import android.os.Handler; +import android.os.Looper; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * OkHttp网络请求工具类 + * @Author ZhanGSKen + * @Date 2026/01/07 + */ +public class OkHttpUtil { + + private static OkHttpClient sOkHttpClient; + private static Handler sMainHandler = new Handler(Looper.getMainLooper()); + + static { + sOkHttpClient = new OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .build(); + } + + /** + * GET请求 + * @param url 请求地址 + * @param callback 回调 + */ + public static void get(String url, final OnResultCallback callback) { + Request request = new Request.Builder() + .url(url) + .get() + .build(); + + sOkHttpClient.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, final IOException e) { + sMainHandler.post(new Runnable() { + @Override + public void run() { + if (callback != null) { + callback.onFailure(e.getMessage()); + } + } + }); + } + + @Override + public void onResponse(Call call, final Response response) throws IOException { + final String result = response.body().string(); + sMainHandler.post(new Runnable() { + @Override + public void run() { + if (callback != null) { + if (response.isSuccessful()) { + callback.onSuccess(result); + } else { + callback.onFailure("请求失败:" + response.code()); + } + } + } + }); + } + }); + } + + /** + * 回调接口 + */ + public interface OnResultCallback { + void onSuccess(String result); + void onFailure(String errorMsg); + } +} + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/utils/RSAUtils.java b/winboll/src/main/java/cc/winboll/studio/winboll/utils/RSAUtils.java new file mode 100644 index 0000000..5258e2b --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/utils/RSAUtils.java @@ -0,0 +1,222 @@ +package cc.winboll.studio.winboll.utils; + +/** + * @Author ZhanGSKen + * @Date 2025/06/04 13:36 + * @Describe RSA加密工具 + */ +import android.content.Context; +import android.util.Base64; +import cc.winboll.studio.libappbase.LogUtils; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Objects; +import javax.crypto.Cipher; + +public class RSAUtils { + private static final String TAG = "RSAUtils"; + private static final int KEY_SIZE = 2048; + private static final String KEY_ALGORITHM = "RSA"; + private static final String PUBLIC_KEY_FILE = "public.key"; + private static final String PRIVATE_KEY_FILE = "private.key"; + private static final String CIPHER_ALGORITHM = KEY_ALGORITHM + "/ECB/PKCS1Padding"; // 保留原加密方式 + + private final String keyPath; + private static volatile RSAUtils INSTANCE; + + /** + * 构造方法:初始化密钥存储路径(内部存储) + */ + private RSAUtils(Context context) { + keyPath = context.getFilesDir() + File.separator + "keys" + File.separator; // 修正路径格式 + } + + /** + * 获取单例实例 + */ + public static synchronized RSAUtils getInstance(Context context) { + if (INSTANCE == null) { + INSTANCE = new RSAUtils(context); + } + return INSTANCE; + } + + /** + * 检查密钥文件是否存在 + */ + public boolean keysExist() { + File publicKeyFile = new File(keyPath + PUBLIC_KEY_FILE); + File privateKeyFile = new File(keyPath + PRIVATE_KEY_FILE); + return publicKeyFile.exists() && privateKeyFile.exists(); + } + + /** + * 生成密钥对并保存到文件 + */ + public void generateAndSaveKeys() throws Exception { + LogUtils.d(TAG, "开始生成 RSA 密钥对(2048位)"); + KeyPairGenerator generator = KeyPairGenerator.getInstance(KEY_ALGORITHM); + generator.initialize(KEY_SIZE); + KeyPair keyPair = generator.generateKeyPair(); + + saveKey(PUBLIC_KEY_FILE, keyPair.getPublic().getEncoded()); + saveKey(PRIVATE_KEY_FILE, keyPair.getPrivate().getEncoded()); + LogUtils.d(TAG, "密钥对生成并保存成功"); + } + + /** + * 获取或生成密钥对(线程安全) + */ + public KeyPair getOrGenerateKeys() throws Exception { + if (!keysExist()) { + synchronized (RSAUtils.class) { // 双重检查锁,避免多线程重复生成 + if (!keysExist()) { + generateAndSaveKeys(); + } + } + } + return readKeysFromFile(); + } + + /** + * 从文件读取密钥对 + */ + private KeyPair readKeysFromFile() throws Exception { + LogUtils.d(TAG, "读取密钥对文件"); + try { + byte[] publicKeyBytes = readFileToBytes(keyPath + PUBLIC_KEY_FILE); + byte[] privateKeyBytes = readFileToBytes(keyPath + PRIVATE_KEY_FILE); + + X509EncodedKeySpec publicSpec = new X509EncodedKeySpec(publicKeyBytes); + PKCS8EncodedKeySpec privateSpec = new PKCS8EncodedKeySpec(privateKeyBytes); + + KeyFactory factory = KeyFactory.getInstance(KEY_ALGORITHM); + PublicKey publicKey = factory.generatePublic(publicSpec); + PrivateKey privateKey = factory.generatePrivate(privateSpec); + + return new KeyPair(publicKey, privateKey); + } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) { + LogUtils.e(TAG, "密钥文件读取失败:" + e.getMessage()); + throw new Exception("密钥文件损坏或格式错误", e); + } + } + + /** + * 保存密钥到文件(通用方法) + */ + private void saveKey(String fileName, byte[] keyBytes) throws IOException { + Objects.requireNonNull(keyBytes, "密钥字节数据不可为空"); + File dir = new File(keyPath); + if (!dir.exists() && !dir.mkdirs()) { + throw new IOException("创建密钥目录失败:" + keyPath); + } + + FileOutputStream fos = null; + try { + fos = new FileOutputStream(keyPath + fileName); + fos.write(keyBytes); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + LogUtils.e(TAG, "关闭文件流失败:" + e.getMessage()); + } + } + } + } + + /** + * 读取文件为字节数组(Java 7 兼容) + */ + private byte[] readFileToBytes(String filePath) throws IOException { + File file = new File(filePath); + if (!file.exists() || file.isDirectory()) { + throw new IOException("文件不存在或为目录:" + filePath); + } + + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + byte[] data = new byte[(int) file.length()]; + int bytesRead = fis.read(data); + if (bytesRead != data.length) { + throw new IOException("文件读取不完整"); + } + return data; + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + LogUtils.e(TAG, "关闭文件流失败:" + e.getMessage()); + } + } + } + } + + /** + * 公钥加密(带参数校验) + */ + public byte[] encryptWithPublicKey(String plainText, PublicKey publicKey) throws Exception { + Objects.requireNonNull(plainText, "明文不可为空"); + Objects.requireNonNull(publicKey, "公钥不可为空"); + + Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + + // 检查数据长度是否超过 RSA 限制(2048位密钥最大明文为 214字节,PKCS1Padding) + int maxPlainTextSize = cipher.getBlockSize() - 11; // PKCS1Padding 固定填充长度 + if (plainText.getBytes("UTF-8").length > maxPlainTextSize) { + throw new IllegalArgumentException("明文过长,最大支持 " + maxPlainTextSize + " 字节"); + } + + return cipher.doFinal(plainText.getBytes("UTF-8")); + } + + /** + * 私钥解密(带参数校验) + */ + public String decryptWithPrivateKey(byte[] encryptedData, PrivateKey privateKey) throws Exception { + Objects.requireNonNull(encryptedData, "密文不可为空"); + Objects.requireNonNull(privateKey, "私钥不可为空"); + + Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + byte[] decryptedBytes = cipher.doFinal(encryptedData); + return new String(decryptedBytes, "UTF-8"); + } + /** + * 将 HTTP 传输的 Base64 字符串还原为加密字节数组(Java 7 兼容) + * @param httpString Base64 字符串(非 null) + * @return 加密字节数组 + * @throws IllegalArgumentException 解码失败时抛出 + */ + public byte[] httpStringToEncryptBytes(String httpString) { + Objects.requireNonNull(httpString, "HTTP 字符串不可为空"); + + // 计算缺失的填充符数量(Java 7 不支持 repeat(),手动拼接) + int pad = httpString.length() % 4; + StringBuilder paddedString = new StringBuilder(httpString); + if (pad != 0) { + for (int i = 0; i < pad; i++) { + paddedString.append('='); // 补全 '=' + } + } + + // 使用 Base64 解码(Android 原生 Base64 类兼容 Java 7) + return Base64.decode(paddedString.toString(), Base64.URL_SAFE); + } +} + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/utils/ServiceUtils.java b/winboll/src/main/java/cc/winboll/studio/winboll/utils/ServiceUtils.java new file mode 100644 index 0000000..74c8065 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/utils/ServiceUtils.java @@ -0,0 +1,35 @@ +package cc.winboll.studio.winboll.utils; + +import android.app.ActivityManager; +import android.content.Context; +import java.util.List; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/27 10:25 + * @Describe ServiceUtils + */ +public class ServiceUtils { + + public static final String TAG = ServiceUtils.class.getSimpleName(); + + public static boolean isServiceAlive(Context context, String szServiceName) { + // 获取Activity管理者对象 + ActivityManager manager = (ActivityManager) context + .getSystemService(Context.ACTIVITY_SERVICE); + // 获取正在运行的服务(此处设置最多取1000个) + List runningServices = manager + .getRunningServices(1000); + if (runningServices.size() <= 0) { + return false; + } + // 遍历,若存在名字和传入的serviceName的一致则说明存在 + for (ActivityManager.RunningServiceInfo runningServiceInfo : runningServices) { + if (runningServiceInfo.service.getClassName().equals(szServiceName)) { + return true; + } + } + + return false; + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/utils/SpecUtil.java b/winboll/src/main/java/cc/winboll/studio/winboll/utils/SpecUtil.java new file mode 100644 index 0000000..279f286 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/utils/SpecUtil.java @@ -0,0 +1,26 @@ +package cc.winboll.studio.winboll.utils; + +import cc.winboll.studio.libappbase.LogUtils; + +/** + * 日志工具类(适配项目规范) + * @Author ZhanGSKen + * @Date 2026/01/07 + */ +public class SpecUtil { + + private static final boolean isDebug = true; + + public static void WWSpecLogInfo(String tag, String msg) { + if (isDebug) { + LogUtils.i(tag, msg); + } + } + + public static void WWSpecLogError(String tag, String msg) { + if (isDebug) { + LogUtils.e(tag, msg); + } + } +} + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/utils/TermuxUtils.java b/winboll/src/main/java/cc/winboll/studio/winboll/utils/TermuxUtils.java new file mode 100644 index 0000000..959169a --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/utils/TermuxUtils.java @@ -0,0 +1,33 @@ +package cc.winboll.studio.winboll.utils; + +/** + * @Author ZhanGSKen + * @Date 2025/06/08 09:05 + * @Describe Termux 应用操作工具集 + */ +import android.content.Intent; + +public abstract class TermuxUtils { + + public static final String TAG = "TermuxUtils"; + + private void runTermuxCommand(String command) { + // 1. 创建 Intent,指定 Termux 的 RunCommandService + /*Intent intent = new Intent("com.termux.RUN_COMMAND"); + intent.setPackage("com.termux"); // Termux 应用的包名 + + // 2. 传递命令参数(必填) + //intent.putExtra("command", command); + intent.putExtra("cd ~/WinBoLL&&echo 'WinBoLL cmd exec at (data", command); + + // 3. 可选:设置工作目录(默认为 Termux 的 home 目录) + intent.putExtra("dir", "/data/data/com.termux/files/home/WinBoLL"); + + // 4. 发送 Intent(需处理可能的安全异常或 ActivityNotFoundException) + try { + getApplicationContext().startService(intent); + } catch (Exception e) { + e.printStackTrace(); + }*/ + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/utils/WxPayApi.java b/winboll/src/main/java/cc/winboll/studio/winboll/utils/WxPayApi.java new file mode 100644 index 0000000..dd0b508 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/utils/WxPayApi.java @@ -0,0 +1,100 @@ +package cc.winboll.studio.winboll.utils; + +import cc.winboll.studio.winboll.WxPayConfig; +import com.alibaba.fastjson.JSONObject; + +/** + * 微信支付服务端接口封装 + * @Author ZhanGSKen + * @Date 2026/01/07 + */ +public class WxPayApi { + + /** + * 统一下单(生成二维码) + * @param callback 回调 + */ + public static void createOrder(final OnCreateOrderCallback callback) { + // 拼接请求参数(服务端测试接口需支持GET传参,若为POST需修改为表单/JSON) + String url = WxPayConfig.CREATE_ORDER_URL + + "?body=" + WxPayConfig.ORDER_BODY + + "&totalFee=" + WxPayConfig.TOTAL_FEE + + "&tradeType=" + WxPayConfig.TRADE_TYPE; + + OkHttpUtil.get(url, new OkHttpUtil.OnResultCallback() { + @Override + public void onSuccess(String result) { + try { + JSONObject jsonObject = JSONObject.parseObject(result); + String outTradeNo = jsonObject.getString("out_trade_no"); + String codeUrl = jsonObject.getString("code_url"); + if (callback != null) { + callback.onSuccess(outTradeNo, codeUrl); + } + } catch (Exception e) { + if (callback != null) { + callback.onFailure("解析统一下单结果失败:" + e.getMessage()); + } + } + } + + @Override + public void onFailure(String errorMsg) { + if (callback != null) { + callback.onFailure("统一下单请求失败:" + errorMsg); + } + } + }); + } + + /** + * 订单查询 + * @param outTradeNo 商户订单号 + * @param callback 回调 + */ + public static void queryOrder(String outTradeNo, final OnQueryOrderCallback callback) { + String url = WxPayConfig.QUERY_ORDER_URL + "?outTradeNo=" + outTradeNo; + + OkHttpUtil.get(url, new OkHttpUtil.OnResultCallback() { + @Override + public void onSuccess(String result) { + try { + JSONObject jsonObject = JSONObject.parseObject(result); + String tradeState = jsonObject.getString("trade_state"); + boolean isSuccess = "SUCCESS".equals(tradeState); + if (callback != null) { + callback.onSuccess(isSuccess, tradeState); + } + } catch (Exception e) { + if (callback != null) { + callback.onFailure("解析订单查询结果失败:" + e.getMessage()); + } + } + } + + @Override + public void onFailure(String errorMsg) { + if (callback != null) { + callback.onFailure("订单查询请求失败:" + errorMsg); + } + } + }); + } + + /** + * 统一下单回调接口 + */ + public interface OnCreateOrderCallback { + void onSuccess(String outTradeNo, String codeUrl); + void onFailure(String errorMsg); + } + + /** + * 订单查询回调接口 + */ + public interface OnQueryOrderCallback { + void onSuccess(boolean isPaySuccess, String tradeState); + void onFailure(String errorMsg); + } +} + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/utils/YunUtils.java b/winboll/src/main/java/cc/winboll/studio/winboll/utils/YunUtils.java new file mode 100644 index 0000000..9c67b01 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/utils/YunUtils.java @@ -0,0 +1,282 @@ +package cc.winboll.studio.winboll.utils; + +/** + * @Author ZhanGSKen + * @Date 2025/06/04 17:21 + * @Describe 应用登录与接口工具 + */ +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.winboll.models.ResponseData; +import cc.winboll.studio.winboll.models.UserInfoModel; +import com.google.gson.Gson; +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.concurrent.TimeUnit; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class YunUtils { + public static final String TAG = "YunUtils"; + // 私有静态实例,类加载时创建 + private static volatile YunUtils INSTANCE; + Context mContext; + UserInfoModel mUserInfoModel; + String token = ""; + String mDataFolderPath = ""; + String mUserInfoModelPath = ""; + + private static final int CONNECT_TIMEOUT = 15; // 连接超时时间(秒) + private static final int READ_TIMEOUT = 20; // 读取超时时间(秒) + private static volatile YunUtils instance; + private OkHttpClient okHttpClient; + private Handler mainHandler; // 主线程 Handler + + // 私有构造方法,防止外部实例化 + private YunUtils(Context context) { + LogUtils.d(TAG, "YunUtils"); + mContext = context; + mDataFolderPath = mContext.getExternalFilesDir(TAG).toString(); + File fTest = new File(mDataFolderPath); + if (!fTest.exists()) { + fTest.mkdirs(); + } + mUserInfoModelPath = mDataFolderPath + File.separator + "UserInfoModel.rsajson"; + + okHttpClient = new OkHttpClient.Builder() + .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS) + .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS) + .build(); + mainHandler = new Handler(Looper.getMainLooper()); // 获取主线程 Looper + } + + // 公共静态方法,返回唯一实例 + public static synchronized YunUtils getInstance(Context context) { + LogUtils.d(TAG, "getInstance"); + if (INSTANCE == null) { + INSTANCE = new YunUtils(context); + } + return INSTANCE; + } + + public void checkLoginStatus() { + String token = getLocalToken(); + LogUtils.d(TAG, String.format("checkLoginStatus token is %s", token)); + } + + String getLocalToken() { + UserInfoModel userInfoModel = loadUserInfoModel(); + return (userInfoModel == null) ?"": userInfoModel.getToken(); + } + + public void login(String host, UserInfoModel userInfoModel) { + LogUtils.d(TAG, "login"); + + // 发送 POST 请求 + String apiUrl = host + "/login/index.php"; + // 序列化对象为JSON + Gson gson = new Gson(); + String jsonData = gson.toJson(userInfoModel); // 自动生成标准JSON + //String jsonData = userInfoModel.toString(); + LogUtils.d(TAG, "要发送的数据 : " + jsonData); + + sendPostRequest(apiUrl, jsonData, new OnResponseListener() { + // 成功回调(主线程) + @Override + public void onSuccess(String responseBody) { + LogUtils.d(TAG, "onSuccess"); + LogUtils.d(TAG, String.format("responseBody %s", responseBody)); + Gson gson = new Gson(); + ResponseData result = gson.fromJson(responseBody, ResponseData.class); // 转为 Result 实例 + if(result.getStatus().equals(ResponseData.STATUS_SUCCESS)) { + + UserInfoModel userInfoModel = result.getData(); + if (userInfoModel != null) { + LogUtils.d(TAG, "收到网站 UserInfoModel"); + String token = userInfoModel.getToken(); + saveLocalToken(token); + checkLoginStatus(); + } + + } else if(result.getStatus().equals(ResponseData.STATUS_ERROR)) { + try { + String decodedMessage = URLDecoder.decode(result.getMessage(), "UTF-8"); + LogUtils.d(TAG, "服务器返回信息: " + decodedMessage); + } catch (UnsupportedEncodingException e) { + LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); + } + } + } + + // 失败回调(主线程) + @Override + public void onFailure(String errorMsg) { + LogUtils.d(TAG, errorMsg); + // 处理错误 + } + }); + } + + public void saveLocalToken(String token) { + UserInfoModel userInfoModel = new UserInfoModel(); + userInfoModel.setToken(token); + saveUserInfoModel(userInfoModel); + } + + UserInfoModel loadUserInfoModel() { +// LogUtils.d(TAG, "loadUserInfoModel"); +// if (new File(mUserInfoModelPath).exists()) { +// try { +// // 加载加密后的模型数据 +// byte[] encryptedData = FileUtils.readByteArrayFromFile(mUserInfoModelPath); +// // 加载 RSA 工具 +// RSAUtils utils = RSAUtils.getInstance(mContext); +// KeyPair keyPair = utils.getOrGenerateKeys(); +// //PublicKey publicKey = keyPair.getPublic(); +// PrivateKey privateKey = keyPair.getPrivate(); +// // 私钥解密模型数据 +// String szInfo = utils.decryptWithPrivateKey(encryptedData, keyPair.getPrivate()); +// LogUtils.d(TAG, String.format("szInfo %s", szInfo)); +// mUserInfoModel = UserInfoModel.parseStringToBean(szInfo, UserInfoModel.class); +// if (mUserInfoModel == null) { +// LogUtils.d(TAG, "模型数据解析为空数据。"); +// } +// LogUtils.d(TAG, "UserInfoModel 解密加载结束。"); +// } catch (Exception e) { +// LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); +// } +// } else { +// LogUtils.d(TAG, "云服务登录信息不存在。"); +// mUserInfoModel = null; +// } +// return mUserInfoModel; + return null; + } + + void saveUserInfoModel(UserInfoModel userInfoModel) { +// LogUtils.d(TAG, "saveUserInfoModel"); +// try { +// String szInfo = userInfoModel.toString(); +// LogUtils.d(TAG, "原始数据: " + szInfo); +// +// RSAUtils utils = RSAUtils.getInstance(mContext); +// KeyPair keyPair = utils.getOrGenerateKeys(); +// PublicKey publicKey = keyPair.getPublic(); +// +// // 公钥加密(传入字节数组,避免中间字符串转换) +// byte[] encryptedData = utils.encryptWithPublicKey(szInfo, publicKey); +// +// // 保存加密字节数组到文件(直接操作字节,无需转字符串) +// FileUtils.writeByteArrayToFile(encryptedData, mUserInfoModelPath); +// LogUtils.d(TAG, "加密数据已保存"); +// +// // 测试解密(仅调试用) +// String szInfo2 = utils.decryptWithPrivateKey(encryptedData, keyPair.getPrivate()); +// LogUtils.d(TAG, "解密结果: " + szInfo2); +// +// mUserInfoModel = UserInfoModel.parseStringToBean(szInfo2, UserInfoModel.class); +// if (mUserInfoModel == null) { +// LogUtils.d(TAG, "模型解析失败"); +// } +// } catch (Exception e) { +// LogUtils.d(TAG, "加密/解密失败: " + e.getMessage()); +// } + } + + // 发送 POST 请求(JSON 数据) + public void sendPostRequest(String url, String data, OnResponseListener listener) { + RequestBody requestBody = RequestBody.create( + MediaType.parse("application/json; charset=utf-8"), // 关键头信息 + data.getBytes(StandardCharsets.UTF_8) + ); + + Request request = new Request.Builder() + .url(url) + .post(requestBody) + .addHeader("Content-Type", "application/json") // 显式添加头 + .build(); + + executeRequest(request, listener); + } + + // 发送 GET 请求 + public void sendGetRequest(String url, OnResponseListener listener) { + Request request = new Request.Builder() + .url(url) + .get() + .build(); + executeRequest(request, listener); + } + + // 执行请求(子线程处理) + private void executeRequest(final Request request, final OnResponseListener listener) { + okHttpClient.newCall(request).enqueue(new Callback() { + // 响应成功(子线程) + @Override + public void onResponse(Call call, Response response) throws IOException { + try { + if (!response.isSuccessful()) { + postFailure(listener, "响应码错误:" + response.code()); + return; + } + String responseBody = response.body().string(); + postSuccess(listener, responseBody); + } catch (Exception e) { + postFailure(listener, "解析失败:" + e.getMessage()); + } + } + + // 响应失败(子线程) + @Override + public void onFailure(Call call, IOException e) { + postFailure(listener, "网络失败:" + e.getMessage()); + } + + // 主线程回调(使用 Handler) + private void postSuccess(final OnResponseListener listener, final String msg) { + mainHandler.post(new Runnable() { + @Override + public void run() { + listener.onSuccess(msg); + } + }); + } + + private void postFailure(final OnResponseListener listener, final String msg) { + mainHandler.post(new Runnable() { + @Override + public void run() { + listener.onFailure(msg); + } + }); + } + }); + } + + public interface OnResponseListener { + /** + * 成功响应(主线程回调) + * @param responseBody 响应体字符串 + */ + void onSuccess(String responseBody); + + /** + * 失败回调(包含错误信息) + * @param errorMsg 错误描述 + */ + void onFailure(String errorMsg); + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/utils/ZXingUtils.java b/winboll/src/main/java/cc/winboll/studio/winboll/utils/ZXingUtils.java new file mode 100644 index 0000000..389298d --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/utils/ZXingUtils.java @@ -0,0 +1,75 @@ +package cc.winboll.studio.winboll.utils; + +import android.graphics.Bitmap; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import com.journeyapps.barcodescanner.BarcodeEncoder; +import java.util.HashMap; +import java.util.Map; + +/** + * ZXing二维码生成工具类 + * 依赖:com.google.zxing:core:3.4.1 + com.journeyapps:zxing-android-embedded:3.6.0 + * @Author ZhanGSKen + * @Date 2026/01/07 + */ +public class ZXingUtils { + + /** + * 生成二维码Bitmap(核心方法,使用journeyapps工具类) + * @param content 内容(如微信支付的code_url) + * @param width 二维码宽度(px) + * @param height 二维码高度(px) + * @return 二维码Bitmap,失败返回null + */ + public static Bitmap createQRCodeBitmap(String content, int width, int height) { + // 1. 入参合法性校验 + if (content == null || content.trim().isEmpty()) { + return null; + } + if (width <= 0 || height <= 0) { + return null; + } + + // 2. 配置二维码参数 + Map hints = new HashMap<>(); + hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); // 字符编码 + hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); // 高容错级别(H级可容忍30%遮挡) + hints.put(EncodeHintType.MARGIN, 1); // 边距(值越小,二维码越紧凑,建议1-4) + + try { + // 3. 生成BitMatrix(二维码矩阵) + QRCodeWriter qrCodeWriter = new QRCodeWriter(); + BitMatrix bitMatrix = qrCodeWriter.encode( + content, + BarcodeFormat.QR_CODE, + width, + height, + hints + ); + + // 4. 转换BitMatrix为Bitmap(关键:使用journeyapps的BarcodeEncoder) + BarcodeEncoder barcodeEncoder = new BarcodeEncoder(); + return barcodeEncoder.createBitmap(bitMatrix); + + } catch (WriterException e) { + e.printStackTrace(); + return null; + } + } + + /** + * 重载方法:生成正方形二维码(宽度=高度) + * @param content 内容 + * @param size 二维码边长(px) + * @return 二维码Bitmap + */ + public static Bitmap createQRCodeBitmap(String content, int size) { + return createQRCodeBitmap(content, size, size); + } +} + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/views/WinBoLLView.java b/winboll/src/main/java/cc/winboll/studio/winboll/views/WinBoLLView.java new file mode 100644 index 0000000..d4dc6f5 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/views/WinBoLLView.java @@ -0,0 +1,232 @@ +package cc.winboll.studio.winboll.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.webkit.WebChromeClient; +import android.webkit.WebResourceError; +import android.webkit.WebResourceRequest; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.ProgressBar; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/27 11:05 + * @Describe 自定义WebView控件(WinBoLLView) + * 集成进度监听、页面加载控制、安全配置等核心能力 + */ +public class WinBoLLView extends WebView { + public static final String TAG = "WinBoLLView"; + + private ProgressBar mProgressBar; // 页面加载进度条(可选,增强用户体验) + private OnPageStatusListener mStatusListener; // 页面状态监听回调 + private boolean mIsLoading = false; // 自定义加载状态标记(替代系统isLoading()) + + // 构造方法(兼容代码创建和XML布局引用) + public WinBoLLView(Context context) { + super(context); + initWebViewSettings(); + initWebViewClient(); + } + + public WinBoLLView(Context context, AttributeSet attrs) { + super(context, attrs); + initWebViewSettings(); + initWebViewClient(); + } + + public WinBoLLView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initWebViewSettings(); + initWebViewClient(); + } + + /** + * 初始化WebView基础配置(安全+性能+兼容性) + */ + private void initWebViewSettings() { + WebSettings settings = getSettings(); + + // 基础功能配置 + settings.setJavaScriptEnabled(true); // 启用JS(根据需求决定是否开启) + settings.setSupportZoom(true); // 支持缩放 + settings.setBuiltInZoomControls(true); // 显示缩放控件 + settings.setDisplayZoomControls(false); // 隐藏系统缩放控件(优化UI) + settings.setLoadWithOverviewMode(true); // 自适应屏幕 + settings.setUseWideViewPort(true); // 支持宽视角 + + // 缓存配置(提升加载速度) + settings.setCacheMode(WebSettings.LOAD_DEFAULT); + settings.setDomStorageEnabled(true); // 启用DOM存储 + settings.setDatabaseEnabled(true); // 启用数据库存储 + + // 安全配置(防止XSS和恶意跳转) + settings.setJavaScriptCanOpenWindowsAutomatically(false); // 禁止JS自动打开窗口 + setBackgroundColor(0x00000000); // 透明背景(避免白屏闪烁) + + // 适配HTTPS和HTTP混合内容(Android 5.0+) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + settings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE); + } + } + + /** + * 初始化WebViewClient和ChromeClient(控制页面加载和进度) + */ + private void initWebViewClient() { + // 控制页面跳转(不打开系统浏览器) + setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + // 拦截URL加载,使用当前WebView打开 + view.loadUrl(request.getUrl().toString()); + return true; + } + + @Override + public void onPageStarted(WebView view, String url, android.graphics.Bitmap favicon) { + super.onPageStarted(view, url, favicon); + // 页面开始加载:更新自定义加载状态标记 + mIsLoading = true; + // 更新进度条+回调状态 + if (mProgressBar != null) mProgressBar.setVisibility(VISIBLE); + if (mStatusListener != null) mStatusListener.onPageStarted(url); + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + // 页面加载完成:更新自定义加载状态标记 + mIsLoading = false; + // 隐藏进度条+回调状态 + if (mProgressBar != null) mProgressBar.setVisibility(GONE); + if (mStatusListener != null) mStatusListener.onPageFinished(url); + } + + @Override + public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { + super.onReceivedError(view, request, error); + // 页面加载错误:更新自定义加载状态标记 + mIsLoading = false; + // 回调错误信息 + if (mStatusListener != null) { + String errorMsg = error.getDescription().toString(); + mStatusListener.onPageError(errorMsg); + } + } + }); + + // 监听页面加载进度 + setWebChromeClient(new WebChromeClient() { + @Override + public void onProgressChanged(WebView view, int newProgress) { + super.onProgressChanged(view, newProgress); + // 更新进度条进度 + if (mProgressBar != null) mProgressBar.setProgress(newProgress); + } + }); + } + + // ------------------- 对外暴露的控制方法(供Fragment调用) ------------------- + /** + * 加载URL(增加空值校验+优先HTTPS协议) + */ + public void loadUrlSafe(String url) { + if (url == null || url.trim().isEmpty()) { + if (mStatusListener != null) mStatusListener.onPageError("URL不能为空"); + return; + } + // 协议补全:优先HTTPS,兼容HTTP(解决ERR_CLEARTEXT_NOT_PERMITTED) + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = "https://" + url; // 改为优先HTTPS,而非HTTP + } + super.loadUrl(url); + } + + /** + * 刷新当前页面(使用自定义mIsLoading判断加载状态) + */ + public void refreshPage() { + if (mIsLoading) { // 替换原isLoading(),使用自定义标记 + stopLoading(); // 若正在加载,先停止再刷新 + } + reload(); + } + + /** + * 停止页面加载(使用自定义mIsLoading判断加载状态) + */ + public void stopPageLoad() { + if (mIsLoading) { // 替换原isLoading(),使用自定义标记 + stopLoading(); + mIsLoading = false; // 停止后更新状态标记 + } + } + + /** + * 前进(判断是否有前进历史) + */ + public boolean goForwardSafe() { + if (canGoForward()) { + goForward(); + return true; + } + return false; + } + + /** + * 后退(判断是否有后退历史) + */ + public boolean goBackSafe() { + if (canGoBack()) { + goBack(); + return true; + } + return false; + } + + // ------------------- 辅助功能:进度条和状态监听 ------------------- + /** + * 设置进度条(绑定Fragment中的进度条控件) + */ + public void setProgressBar(ProgressBar progressBar) { + this.mProgressBar = progressBar; + if (mProgressBar != null) { + mProgressBar.setMax(100); + mProgressBar.setVisibility(GONE); + } + } + + /** + * 设置页面状态监听(供Fragment接收加载状态) + */ + public void setOnPageStatusListener(OnPageStatusListener listener) { + this.mStatusListener = listener; + } + + /** + * 页面状态监听接口 + */ + public interface OnPageStatusListener { + void onPageStarted(String url); // 页面开始加载 + void onPageFinished(String url); // 页面加载完成 + void onPageError(String errorMsg); // 页面加载错误 + } + + // ------------------- 资源释放(防止内存泄漏) ------------------- + /** + * 销毁WebView(必须在Fragment销毁时调用) + */ + public void destroyWebView() { + // 停止加载并清空历史 + stopLoading(); + mIsLoading = false; // 销毁时重置加载状态 + clearHistory(); + // 移除所有WebViewClient(避免内存泄漏) + setWebViewClient(null); + setWebChromeClient(null); + // 销毁WebView + destroy(); + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/widgets/APPNewsWidget.java b/winboll/src/main/java/cc/winboll/studio/winboll/widgets/APPNewsWidget.java new file mode 100644 index 0000000..4796898 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/widgets/APPNewsWidget.java @@ -0,0 +1,185 @@ +package cc.winboll.studio.winboll.widgets; +/** + * @Author ZhanGSKen + * @Date 2025/02/15 14:41:25 + * @Describe TimeWidget + */ +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.widget.RemoteViews; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.winboll.R; +import cc.winboll.studio.winboll.WinBoLL; +import cc.winboll.studio.winboll.models.WinBoLLModel; +import cc.winboll.studio.winboll.models.WinBoLLNewsBean; +import cc.winboll.studio.winboll.receivers.APPNewsWidgetClickListener; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; + +public class APPNewsWidget extends AppWidgetProvider { + + public static final String TAG = "APPNewsWidget"; + + public static final String ACTION_WAKEUP_SERVICE = APPNewsWidget.class.getName() + ".ACTION_WAKEUP_SERVICE"; + public static final String ACTION_RELOAD_REPORT = APPNewsWidget.class.getName() + ".ACTION_RELOAD_REPORT"; + + + volatile static ArrayList _WinBoLLNewsBeanList; + final static int _MAX_PAGES = 10; + final static int _OnePageLinesCount = 5; + volatile static int _CurrentPageIndex = 0; + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + initWinBoLLNewsBeanList(context); + for (int appWidgetId : appWidgetIds) { + updateAppWidget(context, appWidgetManager, appWidgetId); + } + } + + @Override + public void onReceive(Context context, Intent intent) { + super.onReceive(context, intent); + initWinBoLLNewsBeanList(context); + if (intent.getAction().equals(ACTION_RELOAD_REPORT)) { + LogUtils.d(TAG, "ACTION_RELOAD_REPORT"); + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, APPNewsWidget.class)); + for (int appWidgetId : appWidgetIds) { + updateAppWidget(context, appWidgetManager, appWidgetId); + } + }else if (intent.getAction().equals(ACTION_WAKEUP_SERVICE)) { +// LogUtils.d(TAG, "ACTION_WAKEUP_SERVICE"); +// String szWinBoLLModel = intent.getStringExtra(WinBoLL.EXTRA_WINBOLLMODEL); +// LogUtils.d(TAG, String.format("szWinBoLLModel %s", szWinBoLLModel)); +// if (szWinBoLLModel != null && !szWinBoLLModel.equals("")) { +// try { +// WinBoLLModel bean = WinBoLLModel.parseStringToBean(szWinBoLLModel, WinBoLLModel.class); +// if (bean != null) { +// String szAppPackageName = bean.getAppPackageName(); +// LogUtils.d(TAG, String.format("szAppPackageName %s", szAppPackageName)); +// String szAppMainServiveName = bean.getAppMainServiveName(); +// LogUtils.d(TAG, String.format("szAppMainServiveName %s", szAppMainServiveName)); +// +// +// String appName = AppUtils.getAppNameByPackageName(context, szAppPackageName); +// LogUtils.d(TAG, String.format("appName %s", appName)); +// WinBoLLNewsBean winBollNewsBean = new WinBoLLNewsBean(appName); +// SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); +// String currentTime = sdf.format(new Date()); +// StringBuilder sbLine = new StringBuilder(); +// sbLine.append("["); +// sbLine.append(currentTime); +// sbLine.append("] Wake up "); +// sbLine.append(appName); +// winBollNewsBean.setMessage(sbLine.toString()); +// +// addWinBoLLNewsBean(context, winBollNewsBean); +// +// AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); +// int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, APPNewsWidget.class)); +// for (int appWidgetId : appWidgetIds) { +// updateAppWidget(context, appWidgetManager, appWidgetId); +// } +// } +// } catch (IOException e) { +// LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); +// } +// } + } + } + + // + // 加入新报告信息 + // + public synchronized static void addWinBoLLNewsBean(Context context, WinBoLLNewsBean bean) { + initWinBoLLNewsBeanList(context); + _WinBoLLNewsBeanList.add(0, bean); + // 控制记录总数 + while (_WinBoLLNewsBeanList.size() > _MAX_PAGES * _OnePageLinesCount) { + _WinBoLLNewsBeanList.remove(_WinBoLLNewsBeanList.size() - 1); + } + WinBoLLNewsBean.saveBeanList(context, _WinBoLLNewsBeanList, WinBoLLNewsBean.class); + } + + synchronized static void initWinBoLLNewsBeanList(Context context) { + if (_WinBoLLNewsBeanList == null) { + _WinBoLLNewsBeanList = new ArrayList(); + WinBoLLNewsBean.loadBeanList(context, _WinBoLLNewsBeanList, WinBoLLNewsBean.class); + } + if (_WinBoLLNewsBeanList == null) { + _WinBoLLNewsBeanList = new ArrayList(); + WinBoLLNewsBean.saveBeanList(context, _WinBoLLNewsBeanList, WinBoLLNewsBean.class); + } + } + + private void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { + LogUtils.d(TAG, "updateAppWidget(...)"); + + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_news); + //设置按钮点击事件 + Intent intentPre = new Intent(context, APPNewsWidgetClickListener.class); + intentPre.setAction(APPNewsWidgetClickListener.ACTION_PRE); + PendingIntent pendingIntentPre = PendingIntent.getBroadcast(context, 0, intentPre, PendingIntent.FLAG_UPDATE_CURRENT); + views.setOnClickPendingIntent(R.id.widget_button_pre, pendingIntentPre); + Intent intentNext = new Intent(context, APPNewsWidgetClickListener.class); + intentNext.setAction(APPNewsWidgetClickListener.ACTION_NEXT); + PendingIntent pendingIntentNext = PendingIntent.getBroadcast(context, 0, intentNext, PendingIntent.FLAG_UPDATE_CURRENT); + views.setOnClickPendingIntent(R.id.widget_button_next, pendingIntentNext); + + views.setTextViewText(R.id.tv_msg, getPageInfo()); + views.setTextViewText(R.id.tv_news, getMessage()); + appWidgetManager.updateAppWidget(appWidgetId, views); + } + + public static String getMessage() { + ArrayList msgTemp = new ArrayList(); + if (_WinBoLLNewsBeanList != null) { + int start = _OnePageLinesCount * _CurrentPageIndex; + start = _WinBoLLNewsBeanList.size() > start ? start : _WinBoLLNewsBeanList.size() - 1; + for (int i = start, j = 0; i < _WinBoLLNewsBeanList.size() && j < _OnePageLinesCount && start > -1; i++, j++) { + msgTemp.add(_WinBoLLNewsBeanList.get(i).getMessage()); + } + String message = String.join("\n", msgTemp); + return message; + } + return ""; + } + + public static void prePage(Context context) { + if (_WinBoLLNewsBeanList != null) { + if (_CurrentPageIndex > 0) { + _CurrentPageIndex = _CurrentPageIndex - 1; + } + Intent intentWidget = new Intent(context, APPNewsWidget.class); + intentWidget.setAction(APPNewsWidget.ACTION_RELOAD_REPORT); + context.sendBroadcast(intentWidget); + } + } + + public static void nextPage(Context context) { + if (_WinBoLLNewsBeanList != null) { + if ((_CurrentPageIndex + 1) * _OnePageLinesCount < _WinBoLLNewsBeanList.size()) { + _CurrentPageIndex = _CurrentPageIndex + 1; + } + Intent intentWidget = new Intent(context, APPNewsWidget.class); + intentWidget.setAction(APPNewsWidget.ACTION_RELOAD_REPORT); + context.sendBroadcast(intentWidget); + } + } + + String getPageInfo() { + if (_WinBoLLNewsBeanList == null) { + return "0/0"; + } + int leftCount = _WinBoLLNewsBeanList.size() % _OnePageLinesCount; + int currentPageCount = _WinBoLLNewsBeanList.size() / _OnePageLinesCount + (leftCount == 0 ?0: 1); + return String.format("%d/%d", _CurrentPageIndex + 1, currentPageCount); + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/widgets/StatusWidget.java b/winboll/src/main/java/cc/winboll/studio/winboll/widgets/StatusWidget.java new file mode 100644 index 0000000..454a78b --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/widgets/StatusWidget.java @@ -0,0 +1,59 @@ +package cc.winboll.studio.winboll.widgets; + +/** + * @Author ZhanGSKen + * @Date 2025/02/17 20:32:12 + */ +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.widget.RemoteViews; +import cc.winboll.studio.winboll.R; +import cc.winboll.studio.libappbase.ToastUtils; + +public class StatusWidget extends AppWidgetProvider { + + public static final String TAG = "StatusWidget"; + + public static final String ACTION_STATUS_UPDATE = "cc.winboll.studio.libappbase.widgets.APPWidget.ACTION_STATUS_UPDATE"; + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + for (int appWidgetId : appWidgetIds) { + updateAppWidget(context, appWidgetManager, appWidgetId); + } + } + + @Override + public void onReceive(Context context, Intent intent) { + super.onReceive(context, intent); + if (intent.getAction().equals(ACTION_STATUS_UPDATE)) { + ToastUtils.show("Test"); + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, StatusWidget.class)); + for (int appWidgetId : appWidgetIds) { + updateAppWidget(context, appWidgetManager, appWidgetId); + } + } + } + + private void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_status); + //设置按钮点击事件 + Intent intentAppButton = new Intent(context, StatusWidgetClickListener.class); + intentAppButton.setAction(StatusWidgetClickListener.ACTION_IVAPP); + PendingIntent pendingIntentAppButton = PendingIntent.getBroadcast(context, 0, intentAppButton, PendingIntent.FLAG_UPDATE_CURRENT); + views.setOnClickPendingIntent(R.id.ivapp, pendingIntentAppButton); + +// boolean isActive = ServiceUtils.isServiceRunning(context, TestService.class.getName()); +// if (isActive) { +// views.setImageViewResource(R.id.ivapp, cc.winboll.studio.libappbase.R.drawable.ic_launcher); +// } else { +// views.setImageViewResource(R.id.ivapp, cc.winboll.studio.libappbase.R.drawable.ic_launcher_disable); +// } + appWidgetManager.updateAppWidget(appWidgetId, views); + } +} diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/widgets/StatusWidgetClickListener.java b/winboll/src/main/java/cc/winboll/studio/winboll/widgets/StatusWidgetClickListener.java new file mode 100644 index 0000000..ae182ce --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/widgets/StatusWidgetClickListener.java @@ -0,0 +1,33 @@ +package cc.winboll.studio.winboll.widgets; + +/** + * @Author ZhanGSKen + * @Date 2025/02/17 20:33:53 + * @Describe APPWidgetClickListener + */ +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.ToastUtils; + +public class StatusWidgetClickListener extends BroadcastReceiver { + + public static final String TAG = "APPWidgetClickListener"; + + public static final String ACTION_IVAPP = "cc.winboll.studio.libappbase.widgets.StatusWidgetClickListener.ACTION_IVAPP"; + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action == null) { + LogUtils.d(TAG, String.format("action %s", action)); + return; + } + if (action.equals(ACTION_IVAPP)) { + ToastUtils.show("ACTION_LAUNCHER"); + } else { + LogUtils.d(TAG, String.format("action %s", action)); + } + } +} diff --git a/winboll/src/main/java/com/tencent/wework/SpecCallbackSDK.java b/winboll/src/main/java/com/tencent/wework/SpecCallbackSDK.java new file mode 100644 index 0000000..18faee5 --- /dev/null +++ b/winboll/src/main/java/com/tencent/wework/SpecCallbackSDK.java @@ -0,0 +1,210 @@ +package com.tencent.wework; + +import java.util.Map; +import java.util.HashMap;; + +/** + * @warning: 1. 不要修改成员变量名,native方法内有反射调用 + * 2. 调用本地方法需保持包结构,本工具需放在包com.tencent.wework内 + * 3. 不允许继承,类名和函数名均不可修改,会影响本地方法的引用,详见:javah生成本地方法头文件 + */ +public final class SpecCallbackSDK { + + /** + * @description 调用本地方法后实例化的对象指针 + */ + private long specCallbackSDKptr = 0; + + public long GetPtr() { return specCallbackSDKptr; } + + /** + * @description: 回包的headers + */ + private Map responseHeaders; + + public Map GetResponseHeaders() { return responseHeaders; } + + /** + * @description: 回包的加密后的body + */ + private String responseBody; + + public String GetResponseBody() { return responseBody; } + + /** + * @description: 每个请求构造一个SpecCallbackSDK示例, + * SpecCallbackSDK仅持有headers和body的引用, + * 因此需保证headers和body的生存期比SpecCallbackSDK长 + * @param method: 请求方法GET/POST + * @param headers: 请求header + * @param body: 请求body + * @example: + * SpecCallbackSDK sdk = new SpecCallbackSDK(method, headers, body); + * if (sdk.IsOk()) { + * String corpid = sdk.GetCorpId(); + * String agentid = sdk.GetAgentId(); + * String call_type = sdk.GetCallType(); + * String data = sdk.GetData(); + * //do something... + * } + * String response = ...; + * sdk.BuildResponseHeaderBody(response); + * Map responseHeaders = sdk.GetResponseHeaders(); + * String body = sdk.GetResponseBody(); + * //do response + * + * @return errorcode 示例如下: + * -920001: 未设置请求方法 + * -920002: 未设置请求header + * -920003: 未设置请求body + * */ + public SpecCallbackSDK(String method, Map headers, String body) { + try { + specCallbackSDKptr = NewCallbackSDK(method, headers, body); + } catch (Exception e) { + SpecUtil.WWSpecLogError("SpecCallbackSDK exception caught", e.getMessage()); + } + } + + private native long NewCallbackSDK(String method, Map headers, String body); + + /** + * @usage 在Java对象的内存回收前析构C++对象 + */ + @Override + protected void finalize() throws Throwable { + DeleteCPPInstance(specCallbackSDKptr); + super.finalize(); + } + + private native void DeleteCPPInstance(long specCallbackSDKptr); + + /** + * @description: 判断构造函数中传入的请求是否解析成功 + * @return: 成功与否 + * */ + public boolean IsOk() { + return IsOk(specCallbackSDKptr); + } + + private native boolean IsOk(long specCallbackSDKptr); + + /** + * @description: 获取请求的企业 + * @require: 仅当IsOk() == true可调用 + * @return: corpid + * */ + public String GetCorpId() { + return GetCorpId(specCallbackSDKptr); + } + + private native String GetCorpId(long specCallbackSDKptr); + + /** + * @description: 获取请求的应用 + * @require: 仅当IsOk() == true可调用 + * @return: agentid + * */ + public long GetAgentId() { + return GetAgentId(specCallbackSDKptr); + } + + private native long GetAgentId(long specCallbackSDKptr); + + /** + * @description: 获取请求的类型 + * @require: 仅当IsOk() == true可调用 + * @return: 1 - 来自[应用调用专区]的请求 + * 2 - 来自企业微信的回调事件 + * */ + public long GetCallType() { + return GetCallType(specCallbackSDKptr); + } + + private native long GetCallType(long specCallbackSDKptr); + + /** + * @description: 获取请求数据 + * @require: 仅当IsOk() == true可调用 + * @return: 请求数据,根据call_type可能是: + * - 企业微信回调事件 + * - [应用调用专区]接口中的request_data + * */ + public String GetData() { + return GetData(specCallbackSDKptr); + } + + private native String GetData(long specCallbackSDKptr); + + /** + * @description: 是否异步请求 + * @require: 仅当IsOk() == true可调用 + * @return: 是否异步请求 + * */ + public boolean GetIsAsync() { + return GetIsAsync(specCallbackSDKptr); + } + + private native boolean GetIsAsync(long specCallbackSDKptr); + + /** + * @description: 获取请求的job_info, + * @require: 仅当IsOk() == true可调用 + * @return: job_info,无需理解内容, + * 在同一个请求上下文中使用SpecSDK的时候传入 + * */ + public String GetJobInfo() { + return GetJobInfo(specCallbackSDKptr); + } + + private native String GetJobInfo(long specCallbackSDKptr); + + /** + * @description: 获取请求的ability_id,[应用调用专区]接口时指定 + * @require: 仅当IsOk() == true可调用 + * @return: ability_id + * */ + public String GetAbilityId() { + return GetAbilityId(specCallbackSDKptr); + } + + private native String GetAbilityId(long specCallbackSDKptr); + + /** + * @description: 获取请求的notify_id,用于[应用同步调用专区程序]接口 + * @require: 仅当IsOk() == true可调用 + * @return: notify_id + * */ + public String GetNotifyId() { + return GetNotifyId(specCallbackSDKptr); + } + + private native String GetNotifyId(long specCallbackSDKptr); + + /** + * @description: 对返回包计算签名&加密 + * @param response: 待加密的回包明文.如果IsOk()==false,传入空串即可 + * @note 本接口的执行问题可查看日志 + * */ + public void BuildResponseHeaderBody(String response) { + try { + responseHeaders = new HashMap(); + responseBody = ""; + BuildResponseHeaderBody(specCallbackSDKptr, response); + } catch (Exception e) { + SpecUtil.WWSpecLogError("SpecCallbackSDK exception caught", e.getMessage()); + } + } + + private native void BuildResponseHeaderBody(long specCallbackSDKptr, String response); + + // 静态代码块内还无法调用native日志函数,这里的日志在管理系统无法查询 + static { + try { + Class.forName("com.tencent.wework.SpecUtil"); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + System.exit(1); + } + } +} diff --git a/winboll/src/main/java/com/tencent/wework/SpecSDK.java b/winboll/src/main/java/com/tencent/wework/SpecSDK.java new file mode 100644 index 0000000..8cf555a --- /dev/null +++ b/winboll/src/main/java/com/tencent/wework/SpecSDK.java @@ -0,0 +1,163 @@ +package com.tencent.wework; + +/** + * @warning: 1. 不要修改成员变量名,native方法内有反射调用 + * 2. 调用本地方法需保持包结构,本工具需放在包com.tencent.wework内 + * 3. 不允许继承,类名和函数名均不可修改,会影响本地方法的引用,详见:javah生成本地方法头文件 + */ +public final class SpecSDK { + + /** + * @description 调用本地方法后实例化的对象指针 + */ + private long specSDKptr = 0; + + /** + * @usage invoke的请求 + * @example "{\"limit\":1} + */ + private String request; + + public void SetRequest(String request) { + this.request = request; + } + + /** + * @usage 访问上一次invoke的结果 + */ + private String response; + + public String GetResponse() { + return response; + } + + /** + * @param corpid: 企业corpid,必选参数 + * @param agentid: 应用id,必选参数 + * @param ability_id: 能力ID,可选参数 + * @param job_info: job_info,可选参数 + * */ + public SpecSDK(String corpId, long agentId) { + specSDKptr = NewSDK1(corpId, agentId); + } + + private native long NewSDK1(String corpId, long agentId); + + public SpecSDK(String corpId, long agentId, String abilityId) { + specSDKptr = NewSDK2(corpId, agentId, abilityId); + } + + private native long NewSDK2(String corpId, long agentId, String abilityId); + + public SpecSDK(String corpId, long agentId, String abilityId, String jobInfo) { + specSDKptr = NewSDK3(corpId, agentId, abilityId, jobInfo); + } + + private native long NewSDK3(String corpId, long agentId, String abilityId, String jobInfo); + + /** + * @description 使用callback的请求来初始化 + * @param callback_sdk: 要求IsOk()==true + * @return C++内部指针,创建失败时指针仍为0,并输出错误日志 + * */ + public SpecSDK(SpecCallbackSDK callbackSDK) { + specSDKptr = NewSDK4(callbackSDK.GetPtr()); + } + + private native long NewSDK4(long callbackSDK); + + /** + * @usage 在Java对象的内存回收前析构C++对象 + */ + @Override + protected void finalize() throws Throwable { + DeleteCPPInstance(specSDKptr); + super.finalize(); + } + + private native void DeleteCPPInstance(long specSDKptr); + + /** + * @description 用于在专区内调用企业微信接口 + * @param api_name 接口名 + * @param request json格式的请求数据 + * @param response json格式的返回数据 + * @return errorcode 参考如下: + * 0: 成功 + * -910001: SDK没有初始化 + * -910002: 没有设置请求体 + * -910003: 没有设置请求的API + * -910004: 在SDK成员内找不到成员"response",注意lib内有反射机制,不要修改成员变量名 + * -910005: 使用未初始化的callback初始化SDK + * -910006: invoke调用失败,应检查日志查看具体原因 + * -910007: 响应体为空 + * @note 当返回0时,表示没有网络或请求协议层面或调用方法的失败, + * 调用方需继续检查response中的errcode字段确保业务层面的成功 + * + * @usage 当前版本sdk支持的接口列表,每个接口的具体协议请查看企业微信文档: + * https://developer.work.weixin.qq.com/document/path/91201 + * + * +--------------------------------+--------------------------------+ + * |接口名 |描述 | + * |--------------------------------|--------------------------------| + * |program_async_job_call_back |上报异步任务结果 | + * |sync_msg |获取会话记录 | + * |get_group_chat |获取内部群信息 | + * |get_agree_status_single |获取单聊会话同意情况 | + * |get_agree_status_room |获取群聊会话同意情况 | + * |set_hide_sensitiveinfo_config |设置成员会话组件敏感信息隐藏配置| + * |get_hide_sensitiveinfo_config |获取成员会话组件敏感信息隐藏配置| + * |search_chat |会话名称搜索 | + * |search_msg |会话消息搜索 | + * |create_rule |新增关键词规则 | + * |get_rule_list |获取关键词列表 | + * |get_rule_detail |获取关键词规则详情 | + * |update_rule |修改关键词规则 | + * |delete_rule |删除关键词规则 | + * |get_hit_msg_list |获取命中关键词规则的会话记录 | + * |create_sentiment_task |创建情感分析任务 | + * |get_sentiment_result |获取情感分析结果 | + * |create_summary_task |创建摘要提取任务 | + * |get_summary_result |获取摘要提取结果 | + * |create_customer_tag_task |创建标签匹配任务 | + * |get_customer_tag_result |获取标签任务结果 | + * |create_recommend_dialog_task |创建话术推荐任务 | + * |get_recommend_dialog_result |获取话术推荐结果 | + * |create_private_task |创建自定义模型任务 | + * |get_private_task_result |获取自定义模型结果 | + * |(废弃)document_list |获取知识集列表 | + * |create_spam_task |会话反垃圾创建分析任务 | + * |get_spam_result |会话反垃圾获取任务结果 | + * |create_chatdata_export_job |创建会话内容导出任务 | + * |get_chatdata_export_job_status |获取会话内容导出任务结果 | + * |spec_notify_app |专区通知应用 | + * |create_program_task |创建自定义程序任务 | + * |get_program_task_result |获取自定义程序结果 | + * |knowledge_base_list |获取企业授权给应用的知识集列表 | + * |knowledge_base_create |创建知识集 | + * |knowledge_base_detail |获取知识集详情 | + * |knowledge_base_add_doc |添加知识集內容 | + * |knowledge_base_remove_doc |删除知识集內容 | + * |knowledge_base_modify_name |修改知识集名称 | + * |knowledge_base_delete |删除知识集 | + * |search_contact_or_customer |员工或者客户名称搜索 | + * |create_ww_model_task |创建企微通用模型任务 | + * |get_ww_model_result |获取企微通用模型结果 | + * |get_msg_list_by_page_id |page_id获取消息列表 | + * +-----------------------------------------------------------------+ + * */ + public int Invoke(String apiName) { + return Invoke(specSDKptr, apiName, request); + } + + private native int Invoke(long sdk, String apiName, String request); + + // 静态代码块内还无法调用native日志函数,这里的日志在管理系统无法查询 + static { + try { + Class.forName("com.tencent.wework.SpecUtil"); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } +} diff --git a/winboll/src/main/java/com/tencent/wework/SpecUtil.java b/winboll/src/main/java/com/tencent/wework/SpecUtil.java new file mode 100644 index 0000000..85ea23b --- /dev/null +++ b/winboll/src/main/java/com/tencent/wework/SpecUtil.java @@ -0,0 +1,171 @@ +package com.tencent.wework; + +//import java.lang.management.ManagementFactory; +//import java.lang.management.RuntimeMXBean; + +/** + * @warning: 1. 不要修改成员变量名,native方法内有反射调用 + * 2. 调用本地方法需保持包结构,本工具需放在包com.tencent.wework内 + * 3. 不允许继承,类名和函数名均不可修改,会影响本地方法的引用,详见:javah生成本地方法头文件 + * 4. 使用其他工具打印的日志将无法被查询,如需使用SLF4j风格的日志或性能更好的日志框架, + * 请自行封装SpecUtil.SpecLog或SpecUtil.SpecLogNative方法 + * + * @usage: 1. 获取SDK的版本号 + * 2. 打印三个级别的日志 + * 3. 开启调试模式 + */ +public final class SpecUtil { + + /** + * @description SDK版本号 + * @usage 可用于校对不同SDK版本,或后续针对不同的SDK版本添加业务逻辑 + */ + private static final String SDK_VERSION = "1.4.0"; + + public static String GetSDKVersion() { + return SDK_VERSION; + } + + /** + * @description 正确的包名,SDK必须存放在"com.tencent.wework"下,否则会影响本地方法的调用 + */ + private static final String EXPECTED_PACKAGE_NAME = "com.tencent.wework"; + + public static String GetExpectedPackageName() { + return EXPECTED_PACKAGE_NAME; + } + + private static final String LINE_SEPERATOR = System.getProperty("line.separator"); + + public static void WWSpecLogInfo(String... args) { + SpecLog('I', args); + } + + public static void WWSpecLogError(String... args) { + SpecLog('E', args); + } + + public static void WWSpecLogDebug(String... args) { + SpecLog('D', args); + } + + public static void WWSpecLogInfoWithReqId(String reqId, String... args) { + SpecLogWithReqId(reqId, 'I', args); + } + + public static void WWSpecLogErrorWithReqId(String reqId, String... args) { + SpecLogWithReqId(reqId, 'E', args); + } + + public static void WWSpecLogDebugWithReqId(String reqId, String... args) { + SpecLogWithReqId(reqId, 'D', args); + } + + /** + * @usage 打印标准日志 + * @note 只有使用SpecLog和SpecLogNative函数打印的日志能被调试平台查询,其他框架的日志仅能本地查看 + * @param logLevel 日志级别,使用char传递,目前支持I——INFO、E——ERROR、D——DEBUG + * @param args 自定义参数 + */ + public static void SpecLog(char logLevel, String... args) { + StackTraceElement element = Thread.currentThread().getStackTrace()[3]; + SpecLogNative( + logLevel, + element.getFileName(), + element.getLineNumber(), + String.join(",", args).replace(LINE_SEPERATOR, " ") + ); + } + + /** + * @usage 打印标准日志 + * @note 只有使用SpecLog和SpecLogNative函数打印的日志能被调试平台查询,其他框架的日志仅能本地查看 + * @param reqid 请求id + * @param logLevel 日志级别,使用char传递,目前支持I——INFO、E——ERROR、D——DEBUG + * @param args 自定义参数 + */ + public static void SpecLogWithReqId(String reqId, char logLevel, String... args) { + StackTraceElement element = Thread.currentThread().getStackTrace()[3]; + SpecLogNativeWithReqId( + reqId, + logLevel, + element.getFileName(), + element.getLineNumber(), + String.join(",", args).replace(LINE_SEPERATOR, " ") + ); + } + + /** + * @usage 打印标准日志 + * @note 只有使用SpecLog和SpecLogNative函数打印的日志能被调试平台查询,其他框架的日志仅能本地查看 + * 如需SLF4J风格的接口或对日志性能有进一步需求,开发者可以自行封装该函数 + * @param logLevel 日志级别,使用char传递,目前支持I——INFO、E——ERROR、D——DEBUG + * @param fileName 文件名(类名) + * @param lineNumber 行号 + * @param argsString 自定义参数 + */ + public static native void SpecLogNative(char logLevel, String fileName, int lineNumber, String argsString); + + /** + * @usage 打印标准日志 + * @note 只有使用SpecLog和SpecLogNative函数打印的日志能被调试平台查询,其他框架的日志仅能本地查看 + * 如需SLF4J风格的接口或对日志性能有进一步需求,开发者可以自行封装该函数 + * @param reqid 请求id + * @param logLevel 日志级别,使用char传递,目前支持I——INFO、E——ERROR、D——DEBUG + * @param fileName 文件名(类名) + * @param lineNumber 行号 + * @param argsString 自定义参数 + */ + public static native void SpecLogNativeWithReqId(String reqId, char logLevel, String fileName, int lineNumber, String argsString); + + + + /** + * @usage 开启调试模式,进程级别开关 + * @param debugToken 调试凭证,在管理端获取 + * @param accessToken 应用access token + * @return 是否开启成功 + */ + public static boolean SpecOpenDebugMode(String debugToken, String accessToken) { + return SpecOpenDebugModeNative(debugToken, accessToken); + } + + private static native boolean SpecOpenDebugModeNative(String debugToken, String accessToken); + + /** + * @usage 生成notify id。用户可调用本接口生成notify id,也可完全自定义生成 + * @return 新的notify id,支持纳秒级隔离,内部异常时会输出日志并返回空串 + * @note 1. 用户可先生成notify id,将其与回调数据关联存储后,再使用该notify id通知应用, + * 从而保证回调数据被请求时已存储完毕 + */ + public static String GenerateNotifyId() { + return GenerateNotifyIdNative(); + } + + private static native String GenerateNotifyIdNative(); + + static { + // 检查包名 + String packageName = SpecUtil.class.getPackage().getName(); + if (!EXPECTED_PACKAGE_NAME.equals(packageName)) { + // 静态代码块内还无法调用native日志函数,这里的日志在管理系统无法查询 + System.out.println("SpecUtil class must be in package com.tencent.wework"); + System.exit(1); + } + + // 加载so库 + try { + System.loadLibrary("WeWorkSpecSDK"); + } catch (UnsatisfiedLinkError e) { + System.out.println("libWeWorkSpecSDK.so not found in java.library.path"); + e.printStackTrace(); + System.exit(1); + } catch (Exception e) { + System.out.println("unexpected exception: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + + SpecUtil.WWSpecLogInfo("SDK init done", "packageName=" + packageName, "SDK_VERSION=" + SDK_VERSION); + } +} diff --git a/winboll/src/main/res/drawable/bg_browser_top.xml b/winboll/src/main/res/drawable/bg_browser_top.xml new file mode 100644 index 0000000..4762398 --- /dev/null +++ b/winboll/src/main/res/drawable/bg_browser_top.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/winboll/src/main/res/drawable/bg_edittext.xml b/winboll/src/main/res/drawable/bg_edittext.xml new file mode 100644 index 0000000..5bf8014 --- /dev/null +++ b/winboll/src/main/res/drawable/bg_edittext.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/winboll/src/main/res/drawable/bg_shadow.xml b/winboll/src/main/res/drawable/bg_shadow.xml new file mode 100644 index 0000000..6d3d898 --- /dev/null +++ b/winboll/src/main/res/drawable/bg_shadow.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + diff --git a/winboll/src/main/res/drawable/ic_cloud.xml b/winboll/src/main/res/drawable/ic_cloud.xml new file mode 100644 index 0000000..c116648 --- /dev/null +++ b/winboll/src/main/res/drawable/ic_cloud.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/winboll/src/main/res/drawable/ic_cloud_outline.xml b/winboll/src/main/res/drawable/ic_cloud_outline.xml new file mode 100644 index 0000000..a8ed00c --- /dev/null +++ b/winboll/src/main/res/drawable/ic_cloud_outline.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/winboll/src/main/res/drawable/ic_launcher.xml b/winboll/src/main/res/drawable/ic_launcher.xml new file mode 100644 index 0000000..21f28b1 --- /dev/null +++ b/winboll/src/main/res/drawable/ic_launcher.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/winboll/src/main/res/drawable/ic_launcher_background.xml b/winboll/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/winboll/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/winboll/src/main/res/drawable/ic_launcher_beta.xml b/winboll/src/main/res/drawable/ic_launcher_beta.xml new file mode 100644 index 0000000..3dd7f33 --- /dev/null +++ b/winboll/src/main/res/drawable/ic_launcher_beta.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/winboll/src/main/res/drawable/ic_winboll_help.xml b/winboll/src/main/res/drawable/ic_winboll_help.xml new file mode 100644 index 0000000..564175f --- /dev/null +++ b/winboll/src/main/res/drawable/ic_winboll_help.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/winboll/src/main/res/drawable/ic_winboll_jindouyun1.png b/winboll/src/main/res/drawable/ic_winboll_jindouyun1.png new file mode 100644 index 0000000..6b4c005 Binary files /dev/null and b/winboll/src/main/res/drawable/ic_winboll_jindouyun1.png differ diff --git a/winboll/src/main/res/drawable/ic_winboll_jindouyun2.png b/winboll/src/main/res/drawable/ic_winboll_jindouyun2.png new file mode 100644 index 0000000..6f4efa0 Binary files /dev/null and b/winboll/src/main/res/drawable/ic_winboll_jindouyun2.png differ diff --git a/winboll/src/main/res/drawable/ic_winboll_log.xml b/winboll/src/main/res/drawable/ic_winboll_log.xml new file mode 100644 index 0000000..011f2b2 --- /dev/null +++ b/winboll/src/main/res/drawable/ic_winboll_log.xml @@ -0,0 +1,41 @@ + + + + + + + diff --git a/winboll/src/main/res/drawable/ic_winboll_logo.xml b/winboll/src/main/res/drawable/ic_winboll_logo.xml new file mode 100644 index 0000000..ea28987 --- /dev/null +++ b/winboll/src/main/res/drawable/ic_winboll_logo.xml @@ -0,0 +1,48 @@ + + + + + + + + diff --git a/winboll/src/main/res/drawable/ic_winboll_point.xml b/winboll/src/main/res/drawable/ic_winboll_point.xml new file mode 100644 index 0000000..48028cc --- /dev/null +++ b/winboll/src/main/res/drawable/ic_winboll_point.xml @@ -0,0 +1,20 @@ + + + + diff --git a/winboll/src/main/res/drawable/progress_bar_style.xml b/winboll/src/main/res/drawable/progress_bar_style.xml new file mode 100644 index 0000000..7e8f8a3 --- /dev/null +++ b/winboll/src/main/res/drawable/progress_bar_style.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/winboll/src/main/res/drawable/shape_gradient.xml b/winboll/src/main/res/drawable/shape_gradient.xml new file mode 100644 index 0000000..c164fe9 --- /dev/null +++ b/winboll/src/main/res/drawable/shape_gradient.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/winboll/src/main/res/drawable/view_border.xml b/winboll/src/main/res/drawable/view_border.xml new file mode 100644 index 0000000..58b374a --- /dev/null +++ b/winboll/src/main/res/drawable/view_border.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/winboll/src/main/res/layout/activity_logon.xml b/winboll/src/main/res/layout/activity_logon.xml new file mode 100644 index 0000000..2a2c2d1 --- /dev/null +++ b/winboll/src/main/res/layout/activity_logon.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + +