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