diff --git a/.winboll/bashPublishAPKAddTag.sh b/.winboll/bashPublishAPKAddTag.sh
index 0a51077..d210d28 100644
--- a/.winboll/bashPublishAPKAddTag.sh
+++ b/.winboll/bashPublishAPKAddTag.sh
@@ -113,10 +113,10 @@ if [[ $? -eq 0 ]]; then
# 如果Git已经提交了所有代码就执行标签和应用发布操作
# 预先询问是否添加工作流标签
- echo "Add Github Workflows Tag? (yes/No)"
- result=$(askAddWorkflowsTag)
- nAskAddWorkflowsTag=$?
- echo $result
+ #echo "Add Github Workflows Tag? (yes/No)"
+ #result=$(askAddWorkflowsTag)
+ #nAskAddWorkflowsTag=$?
+ #echo $result
# 发布应用
echo "Publishing WinBoLL APK ..."
@@ -138,17 +138,17 @@ if [[ $? -eq 0 ]]; then
fi
# 添加 GitHub 工作流标签
- if [[ $nAskAddWorkflowsTag -eq 1 ]]; then
+ #if [[ $nAskAddWorkflowsTag -eq 1 ]]; then
# 如果用户选择添加工作流标签
- result=$(addWorkflowsTag $1)
- if [[ $? -eq 0 ]]; then
- echo $result
+ #result=$(addWorkflowsTag $1)
+ #if [[ $? -eq 0 ]]; then
+ # echo $result
# 工作流标签添加成功
- else
- echo -e "${0}: addWorkflowsTag $1\n${result}\nAdd workflows tag cancel."
- exit 1 # addWorkflowsTag 异常
- fi
- fi
+ #else
+ #echo -e "${0}: addWorkflowsTag $1\n${result}\nAdd workflows tag cancel."
+ #exit 1 # addWorkflowsTag 异常
+ #fi
+ #fi
## 清理更新描述文件内容
echo "" > $1/app_update_description.txt
diff --git a/README.md b/README.md
index 67c6f0f..1d0852a 100644
--- a/README.md
+++ b/README.md
@@ -114,9 +114,11 @@
# 本项目要实际运用需要注意以下几个步骤:
# 在项目根目录下:
-## 1. 项目模块编译环境设置(必须),settings.gradle-demo 要复制为 settings.gradle,并取消相应项目模块的注释。
-## 2. 项目 Android SDK 编译环境设置(可选),local.properties-demo 要复制为 local.properties,并按需要设置 Android SDK 目录。
-## 3. 类库型模块编译环境设置(可选),winboll.properties-demo 要复制为 winboll.properties,并按需要设置 WinBoLL Maven 库登录用户信息。
+## ★. 项目模块编译环境设置(必须),settings.gradle-demo 要复制为 settings.gradle,并取消相应项目模块的注释。
+## ★. 应用签名密钥 keystore 设置问题。一般调试编译只需用【Termux】cd 进 GenKeyStore 目录执行 $ bash gen_debug_keystore.sh 命令即可完成设置。
+## ☆. 应用 WiBoLL 签名密钥配置问题<非必须考虑>。设置时需要 clone 【keystore】模块源码并拷贝模块目录的 appkey.jks 与 appkey.keystore 到项目根目录即可。
+## ☆. 项目 Android SDK 编译环境设置(可选),local.properties-demo 要复制为 local.properties,并按需要设置 Android SDK 目录。
+## ☆. 类库型模块编译环境设置(可选),winboll.properties-demo 要复制为 winboll.properties,并按需要设置 WinBoLL Maven 库登录用户信息。
# ☆类库型项目编译方法
diff --git a/aes/build.gradle b/aes/build.gradle
index cd6188d..67c1a20 100644
--- a/aes/build.gradle
+++ b/aes/build.gradle
@@ -29,7 +29,7 @@ android {
// versionName 更新后需要手动设置
// 项目模块目录的 build.gradle 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
- versionName "15.8"
+ versionName "15.9"
if(true) {
versionName = genVersionName("${versionName}")
}
diff --git a/aes/build.properties b/aes/build.properties
index 28b69bc..db9036a 100644
--- a/aes/build.properties
+++ b/aes/build.properties
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
-#Tue Jun 03 19:17:05 HKT 2025
-stageCount=2
+#Mon Jun 09 01:44:28 HKT 2025
+stageCount=1
libraryProject=libaes
-baseVersion=15.8
-publishVersion=15.8.1
+baseVersion=15.9
+publishVersion=15.9.0
buildCount=0
-baseBetaVersion=15.8.2
+baseBetaVersion=15.9.1
diff --git a/appbase/build.properties b/appbase/build.properties
index e47cbec..6e794c5 100644
--- a/appbase/build.properties
+++ b/appbase/build.properties
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
-#Tue Jun 03 13:40:08 HKT 2025
-stageCount=5
+#Mon Jun 09 09:38:19 HKT 2025
+stageCount=9
libraryProject=libappbase
baseVersion=15.8
-publishVersion=15.8.4
+publishVersion=15.8.8
buildCount=0
-baseBetaVersion=15.8.5
+baseBetaVersion=15.8.9
diff --git a/appbase/src/main/AndroidManifest.xml b/appbase/src/main/AndroidManifest.xml
index e8a8bdb..66f68a3 100644
--- a/appbase/src/main/AndroidManifest.xml
+++ b/appbase/src/main/AndroidManifest.xml
@@ -9,7 +9,8 @@
android:label="@string/app_name"
android:theme="@style/MyAPPBaseTheme"
android:resizeableActivity="true"
- android:process=":App">
+ android:process=":App"
+ android:networkSecurityConfig="@xml/network_security_config">
-
-
@@ -105,7 +108,8 @@
-
@@ -122,7 +126,6 @@
android:name="android.max_aspect"
android:value="4.0"/>
-
diff --git a/appbase/src/main/java/cc/winboll/studio/appbase/MainActivity.java b/appbase/src/main/java/cc/winboll/studio/appbase/MainActivity.java
index 0b3a8a4..7f55f8a 100644
--- a/appbase/src/main/java/cc/winboll/studio/appbase/MainActivity.java
+++ b/appbase/src/main/java/cc/winboll/studio/appbase/MainActivity.java
@@ -62,6 +62,11 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
+ if(item.getItemId() == R.id.item_yun) {
+ GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(this, cc.winboll.studio.libappbase.activities.YunActivity.class);
+ } else if(item.getItemId() == R.id.item_logon) {
+ GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(this, cc.winboll.studio.libappbase.activities.LogonActivity.class);
+ }
// 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。
return super.onOptionsItemSelected(item);
}
diff --git a/appbase/src/main/res/menu/toolbar_main.xml b/appbase/src/main/res/menu/toolbar_main.xml
index 7fac644..41657dd 100644
--- a/appbase/src/main/res/menu/toolbar_main.xml
+++ b/appbase/src/main/res/menu/toolbar_main.xml
@@ -5,6 +5,14 @@
android:id="@+id/item_home"
android:title="HOME"
android:icon="@drawable/ic_winboll"/>
+
+
-
+
+
+
+ winboll.cc
+
+
+
+
+ 10.8.0.250
+
+
diff --git a/libaes/build.properties b/libaes/build.properties
index c45bc0d..db9036a 100644
--- a/libaes/build.properties
+++ b/libaes/build.properties
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
-#Tue Jun 03 19:16:41 HKT 2025
-stageCount=2
+#Mon Jun 09 01:44:28 HKT 2025
+stageCount=1
libraryProject=libaes
-baseVersion=15.8
-publishVersion=15.8.1
+baseVersion=15.9
+publishVersion=15.9.0
buildCount=0
-baseBetaVersion=15.8.2
+baseBetaVersion=15.9.1
diff --git a/libappbase/build.gradle b/libappbase/build.gradle
index 39bd64b..c5346fe 100644
--- a/libappbase/build.gradle
+++ b/libappbase/build.gradle
@@ -22,4 +22,9 @@ android {
dependencies {
api fileTree(dir: 'libs', include: ['*.jar'])
+ // 网络连接类库
+ api 'com.squareup.okhttp3:okhttp:4.4.1'
+ // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind
+
+ api 'com.google.code.gson:gson:2.10.1'
}
diff --git a/libappbase/build.properties b/libappbase/build.properties
index d7034ef..6e794c5 100644
--- a/libappbase/build.properties
+++ b/libappbase/build.properties
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
-#Tue Jun 03 13:40:01 HKT 2025
-stageCount=5
+#Mon Jun 09 09:38:19 HKT 2025
+stageCount=9
libraryProject=libappbase
baseVersion=15.8
-publishVersion=15.8.4
+publishVersion=15.8.8
buildCount=0
-baseBetaVersion=15.8.5
+baseBetaVersion=15.8.9
diff --git a/libappbase/src/main/AndroidManifest.xml b/libappbase/src/main/AndroidManifest.xml
index dee879c..353ddce 100644
--- a/libappbase/src/main/AndroidManifest.xml
+++ b/libappbase/src/main/AndroidManifest.xml
@@ -103,6 +103,10 @@
+
+
+
+
diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/activities/LogonActivity.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/activities/LogonActivity.java
new file mode 100644
index 0000000..767d570
--- /dev/null
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/activities/LogonActivity.java
@@ -0,0 +1,150 @@
+package cc.winboll.studio.libappbase.activities;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.RadioButton;
+import cc.winboll.studio.libappbase.BuildConfig;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.libappbase.LogView;
+import cc.winboll.studio.libappbase.R;
+import cc.winboll.studio.libappbase.models.UserInfoModel;
+import cc.winboll.studio.libappbase.utils.RSAUtils;
+import cc.winboll.studio.libappbase.utils.YunUtils;
+import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
+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/libappbase/src/main/java/cc/winboll/studio/libappbase/activities/YunActivity.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/activities/YunActivity.java
new file mode 100644
index 0000000..c4e93e7
--- /dev/null
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/activities/YunActivity.java
@@ -0,0 +1,126 @@
+package cc.winboll.studio.libappbase.activities;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+import cc.winboll.studio.libappbase.BuildConfig;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.libappbase.R;
+import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
+import java.io.IOException;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import android.widget.RadioButton;
+import cc.winboll.studio.libappbase.LogView;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/06/04 11:06
+ * @Describe 云宝云
+ */
+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/libappbase/src/main/java/cc/winboll/studio/libappbase/models/ResponseData.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/models/ResponseData.java
new file mode 100644
index 0000000..675f40b
--- /dev/null
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/models/ResponseData.java
@@ -0,0 +1,53 @@
+package cc.winboll.studio.libappbase.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/libappbase/src/main/java/cc/winboll/studio/libappbase/models/UserInfoModel.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/models/UserInfoModel.java
new file mode 100644
index 0000000..3affb45
--- /dev/null
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/models/UserInfoModel.java
@@ -0,0 +1,92 @@
+package cc.winboll.studio.libappbase.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/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/FileUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/FileUtils.java
new file mode 100644
index 0000000..b56f263
--- /dev/null
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/FileUtils.java
@@ -0,0 +1,128 @@
+package cc.winboll.studio.libappbase.utils;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/06/04 20:15
+ * @Describe 文件操作类
+ */
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+
+public class FileUtils {
+ public static final String TAG = "FileUtils";
+
+ /**
+ * 读取文件为字节数组(Java 7 语法)
+ */
+ public static byte[] readFileToByteArray(String filePath) {
+ FileInputStream fis = null;
+ ByteArrayOutputStream bos = null;
+ try {
+ fis = new FileInputStream(filePath);
+ bos = new ByteArrayOutputStream();
+ byte[] buffer = new byte[4096];
+ int bytesRead;
+ while ((bytesRead = fis.read(buffer)) != -1) {
+ bos.write(buffer, 0, bytesRead);
+ }
+ return bos.toByteArray();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ } finally {
+ // 手动关闭流(Java 7 不支持 try-with-resources)
+ if (fis != null) {
+ try {
+ fis.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ if (bos != null) {
+ try {
+ bos.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ /**
+ * 写入字节数组到文件(Java 7 语法)
+ */
+ public static boolean writeByteArrayToFile(byte[] data, String filePath) {
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(filePath);
+ fos.write(data);
+ return true;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ // 原字符串读写方法(适配 Java 7)
+ public static String readFileToString(String filePath) {
+ BufferedReader reader = null;
+ try {
+ reader = new BufferedReader(new FileReader(filePath));
+ StringBuilder content = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ content.append(line).append(System.getProperty("line.separator"));
+ }
+ // 去除最后一个换行符(可选)
+ if (content.length() > 0) {
+ content.deleteCharAt(content.length() - 1);
+ }
+ return content.toString();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ } finally {
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ public static boolean writeStringToFile(String content, String filePath, boolean append) {
+ BufferedWriter writer = null;
+ try {
+ writer = new BufferedWriter(new FileWriter(filePath, append));
+ writer.write(content);
+ return true;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ } finally {
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+}
diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/RSAUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/RSAUtils.java
new file mode 100644
index 0000000..e5c58c3
--- /dev/null
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/RSAUtils.java
@@ -0,0 +1,222 @@
+package cc.winboll.studio.libappbase.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/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/YunUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/YunUtils.java
new file mode 100644
index 0000000..7858da2
--- /dev/null
+++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/YunUtils.java
@@ -0,0 +1,281 @@
+package cc.winboll.studio.libappbase.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.libappbase.models.ResponseData;
+import cc.winboll.studio.libappbase.models.UserInfoModel;
+import com.google.gson.Gson;
+import java.io.File;
+import java.io.IOException;
+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;
+import java.io.UnsupportedEncodingException;
+
+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.readFileToByteArray(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;
+ }
+
+ 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/libappbase/src/main/res/layout/activity_logon.xml b/libappbase/src/main/res/layout/activity_logon.xml
new file mode 100644
index 0000000..2a2c2d1
--- /dev/null
+++ b/libappbase/src/main/res/layout/activity_logon.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libappbase/src/main/res/layout/activity_yun.xml b/libappbase/src/main/res/layout/activity_yun.xml
new file mode 100644
index 0000000..ac73fab
--- /dev/null
+++ b/libappbase/src/main/res/layout/activity_yun.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/numtable/.gitignore b/numtable/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/numtable/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/numtable/README.md b/numtable/README.md
new file mode 100644
index 0000000..f26c1ba
--- /dev/null
+++ b/numtable/README.md
@@ -0,0 +1,34 @@
+# NumTable
+
+#### 介绍
+桌面图标多元应用。提供一个数字风格化的桌面标识图标,快捷的桌面标识创建途径。主要应用于桌面繁多时的页面环境辅助识别。
+
+#### 软件架构
+适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
+也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
+
+
+#### Gradle 编译说明
+调试版编译命令 :gradle assembleBetaDebug
+阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh numtable
+
+#### 使用说明
+
+#### 参与贡献
+
+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/numtable/app_update_description.txt b/numtable/app_update_description.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/numtable/app_update_description.txt
@@ -0,0 +1 @@
+
diff --git a/numtable/build.gradle b/numtable/build.gradle
new file mode 100644
index 0000000..0df3701
--- /dev/null
+++ b/numtable/build.gradle
@@ -0,0 +1,73 @@
+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 {
+ compileSdkVersion 32
+ buildToolsVersion "32.0.0"
+
+ defaultConfig {
+ applicationId "cc.winboll.studio.numtable"
+ minSdkVersion 24
+ targetSdkVersion 30
+ versionCode 1
+ // versionName 更新后需要手动设置
+ // .winboll/winbollBuildProps.properties 文件的 stageCount=0
+ // Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
+ versionName "15.1"
+ if(true) {
+ versionName = genVersionName("${versionName}")
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ api fileTree(dir: 'libs', include: ['*.jar'])
+
+ // 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.github.getActivity:ToastUtils:10.5'
+ // 网络连接类库
+ api 'com.squareup.okhttp3:okhttp:4.4.1'
+ // 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 'cc.winboll.studio:libaes:15.8.0'
+ api 'cc.winboll.studio:libapputils:15.8.2'
+ api 'cc.winboll.studio:libappbase:15.8.2'
+}
diff --git a/numtable/build.properties b/numtable/build.properties
new file mode 100644
index 0000000..74c8093
--- /dev/null
+++ b/numtable/build.properties
@@ -0,0 +1,8 @@
+#Created by .winboll/winboll_app_build.gradle
+#Sun Jun 08 21:21:11 HKT 2025
+stageCount=1
+libraryProject=
+baseVersion=15.1
+publishVersion=15.1.0
+buildCount=0
+baseBetaVersion=15.1.1
diff --git a/numtable/proguard-rules.pro b/numtable/proguard-rules.pro
new file mode 100644
index 0000000..64b4a05
--- /dev/null
+++ b/numtable/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/numtable/src/beta/AndroidManifest.xml b/numtable/src/beta/AndroidManifest.xml
new file mode 100644
index 0000000..be35225
--- /dev/null
+++ b/numtable/src/beta/AndroidManifest.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/numtable/src/beta/res/values/strings.xml b/numtable/src/beta/res/values/strings.xml
new file mode 100644
index 0000000..1c9574b
--- /dev/null
+++ b/numtable/src/beta/res/values/strings.xml
@@ -0,0 +1,6 @@
+
+
+
+ NumTable +
+
+
diff --git a/numtable/src/main/AndroidManifest.xml b/numtable/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..284eb50
--- /dev/null
+++ b/numtable/src/main/AndroidManifest.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/numtable/src/main/java/cc/winboll/studio/numtable/App.java b/numtable/src/main/java/cc/winboll/studio/numtable/App.java
new file mode 100644
index 0000000..35d6f17
--- /dev/null
+++ b/numtable/src/main/java/cc/winboll/studio/numtable/App.java
@@ -0,0 +1,345 @@
+package cc.winboll.studio.numtable;
+
+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 com.hjq.toast.ToastUtils;
+import com.hjq.toast.style.WhiteToastStyle;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class App extends GlobalApplication {
+
+ private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ // 初始化 Toast 框架
+ ToastUtils.init(this);
+ // 设置 Toast 布局样式
+ //ToastUtils.setView(R.layout.view_toast);
+ ToastUtils.setStyle(new WhiteToastStyle());
+ ToastUtils.setGravity(Gravity.BOTTOM, 0, 200);
+
+ //CrashHandler.getInstance().registerGlobal(this);
+ //CrashHandler.getInstance().registerPart(this);
+ }
+
+ public static void write(InputStream input, OutputStream output) throws IOException {
+ byte[] buf = new byte[1024 * 8];
+ int len;
+ while ((len = input.read(buf)) != -1) {
+ output.write(buf, 0, len);
+ }
+ }
+
+ public static void write(File file, byte[] data) throws IOException {
+ File parent = file.getParentFile();
+ if (parent != null && !parent.exists()) parent.mkdirs();
+
+ ByteArrayInputStream input = new ByteArrayInputStream(data);
+ FileOutputStream output = new FileOutputStream(file);
+ try {
+ write(input, output);
+ } finally {
+ closeIO(input, output);
+ }
+ }
+
+ public static String toString(InputStream input) throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ write(input, output);
+ try {
+ return output.toString("UTF-8");
+ } finally {
+ closeIO(input, output);
+ }
+ }
+
+ public static void closeIO(Closeable... closeables) {
+ for (Closeable closeable : closeables) {
+ try {
+ if (closeable != null) closeable.close();
+ } catch (IOException ignored) {}
+ }
+ }
+
+ public static class CrashHandler {
+
+ public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
+
+ private static CrashHandler sInstance;
+
+ private PartCrashHandler mPartCrashHandler;
+
+ public static CrashHandler getInstance() {
+ if (sInstance == null) {
+ sInstance = new CrashHandler();
+ }
+ return sInstance;
+ }
+
+ public void registerGlobal(Context context) {
+ registerGlobal(context, null);
+ }
+
+ public void registerGlobal(Context context, String crashDir) {
+ Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir));
+ }
+
+ public void unregister() {
+ Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER);
+ }
+
+ public void registerPart(Context context) {
+ unregisterPart(context);
+ mPartCrashHandler = new PartCrashHandler(context.getApplicationContext());
+ MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler);
+ }
+
+ public void unregisterPart(Context context) {
+ if (mPartCrashHandler != null) {
+ mPartCrashHandler.isRunning.set(false);
+ mPartCrashHandler = null;
+ }
+ }
+
+ private static class PartCrashHandler implements Runnable {
+
+ private final Context mContext;
+
+ public AtomicBoolean isRunning = new AtomicBoolean(true);
+
+ public PartCrashHandler(Context context) {
+ this.mContext = context;
+ }
+
+ @Override
+ public void run() {
+ while (isRunning.get()) {
+ try {
+ Looper.loop();
+ } catch (final Throwable e) {
+ e.printStackTrace();
+ if (isRunning.get()) {
+ MAIN_HANDLER.post(new Runnable(){
+
+ @Override
+ public void run() {
+ Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show();
+ }
+ });
+ } else {
+ if (e instanceof RuntimeException) {
+ throw (RuntimeException)e;
+ } else {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler {
+
+ private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss");
+
+ private final Context mContext;
+
+ private final File mCrashDir;
+
+ public UncaughtExceptionHandlerImpl(Context context, String crashDir) {
+ this.mContext = context;
+ this.mCrashDir = TextUtils.isEmpty(crashDir) ? new File(mContext.getExternalCacheDir(), "crash") : new File(crashDir);
+ }
+
+ @Override
+ public void uncaughtException(Thread thread, Throwable throwable) {
+ try {
+
+ String log = buildLog(throwable);
+ writeLog(log);
+
+ try {
+ Intent intent = new Intent(mContext, CrashActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(Intent.EXTRA_TEXT, log);
+ mContext.startActivity(intent);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ writeLog(e.toString());
+ }
+
+ throwable.printStackTrace();
+ android.os.Process.killProcess(android.os.Process.myPid());
+ System.exit(0);
+
+ } catch (Throwable e) {
+ if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
+ }
+ }
+
+ private String buildLog(Throwable throwable) {
+ String time = DATE_FORMAT.format(new Date());
+
+ String versionName = "unknown";
+ long versionCode = 0;
+ try {
+ PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
+ versionName = packageInfo.versionName;
+ versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
+ } catch (Throwable ignored) {}
+
+ LinkedHashMap 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/numtable/src/main/java/cc/winboll/studio/numtable/MainActivity.java b/numtable/src/main/java/cc/winboll/studio/numtable/MainActivity.java
new file mode 100644
index 0000000..b16fbca
--- /dev/null
+++ b/numtable/src/main/java/cc/winboll/studio/numtable/MainActivity.java
@@ -0,0 +1,31 @@
+package cc.winboll.studio.numtable;
+
+import android.os.Bundle;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import cc.winboll.studio.libappbase.LogView;
+import com.hjq.toast.ToastUtils;
+
+public class MainActivity extends AppCompatActivity {
+
+ LogView mLogView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ Toolbar toolbar=(Toolbar)findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ mLogView = findViewById(R.id.logview);
+
+ ToastUtils.show("onCreate");
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mLogView.start();
+ }
+}
diff --git a/numtable/src/main/res/drawable-v24/ic_launcher_foreground.xml b/numtable/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..c7bd21d
--- /dev/null
+++ b/numtable/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/numtable/src/main/res/drawable/ic_launcher.xml b/numtable/src/main/res/drawable/ic_launcher.xml
new file mode 100644
index 0000000..2e7615b
--- /dev/null
+++ b/numtable/src/main/res/drawable/ic_launcher.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/numtable/src/main/res/drawable/ic_launcher_background.xml b/numtable/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..b388876
--- /dev/null
+++ b/numtable/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/numtable/src/main/res/drawable/ic_launcher_beta.xml b/numtable/src/main/res/drawable/ic_launcher_beta.xml
new file mode 100644
index 0000000..8abb38b
--- /dev/null
+++ b/numtable/src/main/res/drawable/ic_launcher_beta.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/numtable/src/main/res/layout/activity_main.xml b/numtable/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..1c21368
--- /dev/null
+++ b/numtable/src/main/res/layout/activity_main.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/numtable/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/numtable/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/numtable/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/numtable/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/numtable/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/numtable/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/numtable/src/main/res/mipmap-hdpi/ic_launcher.png b/numtable/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a2f5908
Binary files /dev/null and b/numtable/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/numtable/src/main/res/mipmap-hdpi/ic_launcher_round.png b/numtable/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..1b52399
Binary files /dev/null and b/numtable/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/numtable/src/main/res/mipmap-mdpi/ic_launcher.png b/numtable/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..ff10afd
Binary files /dev/null and b/numtable/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/numtable/src/main/res/mipmap-mdpi/ic_launcher_round.png b/numtable/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..115a4c7
Binary files /dev/null and b/numtable/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/numtable/src/main/res/mipmap-xhdpi/ic_launcher.png b/numtable/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..dcd3cd8
Binary files /dev/null and b/numtable/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/numtable/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/numtable/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..459ca60
Binary files /dev/null and b/numtable/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/numtable/src/main/res/mipmap-xxhdpi/ic_launcher.png b/numtable/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..8ca12fe
Binary files /dev/null and b/numtable/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/numtable/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/numtable/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..8e19b41
Binary files /dev/null and b/numtable/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/numtable/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/numtable/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b824ebd
Binary files /dev/null and b/numtable/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/numtable/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/numtable/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..4c19a13
Binary files /dev/null and b/numtable/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/numtable/src/main/res/values/colors.xml b/numtable/src/main/res/values/colors.xml
new file mode 100644
index 0000000..479769a
--- /dev/null
+++ b/numtable/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #009688
+ #00796B
+ #FF9800
+
\ No newline at end of file
diff --git a/numtable/src/main/res/values/strings.xml b/numtable/src/main/res/values/strings.xml
new file mode 100644
index 0000000..3e650f7
--- /dev/null
+++ b/numtable/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ NumTable
+
+
diff --git a/numtable/src/main/res/values/styles.xml b/numtable/src/main/res/values/styles.xml
new file mode 100644
index 0000000..a70e242
--- /dev/null
+++ b/numtable/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/numtable/src/stage/AndroidManifest.xml b/numtable/src/stage/AndroidManifest.xml
new file mode 100644
index 0000000..ee78d9f
--- /dev/null
+++ b/numtable/src/stage/AndroidManifest.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/numtable/src/stage/res/values/strings.xml b/numtable/src/stage/res/values/strings.xml
new file mode 100644
index 0000000..ace0c41
--- /dev/null
+++ b/numtable/src/stage/res/values/strings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/settings.gradle-demo b/settings.gradle-demo
index cd0d165..fcf8de4 100644
--- a/settings.gradle-demo
+++ b/settings.gradle-demo
@@ -52,3 +52,7 @@
// Ollama 项目编译设置
//include ':ollama'
//rootProject.name = "ollama"
+
+// NumTable 项目编译设置
+//include ':numtable'
+//rootProject.name = "numtable"