Compare commits

..

16 Commits

Author SHA1 Message Date
ZhanGSKen
74b9350a6a <aes>APK 15.9.0 release Publish. 2025-06-09 01:44:30 +08:00
ZhanGSKen
d2858f23f7 准备与NumTable模块项目合并。先提升AES模块版本号,掌控模块源码版本控制优先权。 2025-06-09 01:42:49 +08:00
ZhanGSKen
7e2ad0c01d 精简代码 2025-06-05 17:00:28 +08:00
ZhanGSKen
476ce02fc8 Json数据适配吻合 2025-06-05 15:06:55 +08:00
ZhanGSKen
bc697279ad 编译参数修复 2025-06-05 12:03:14 +08:00
ZhanGSKen
dee01f1179 接口初步对接,正在调试数据处理方法... 2025-06-05 11:59:36 +08:00
ZhanGSKen
a500decc7a 登录数据加密解密成功 2025-06-05 09:52:04 +08:00
ZhanGSKen
5099d00050 <appbase>APK 15.8.7 release Publish. 2025-06-04 15:04:39 +08:00
ZhanGSKen
515d14e896 编译测试 2025-06-04 15:04:01 +08:00
ZhanGSKen
f630e27ed8 编译参数修复 2025-06-04 15:03:06 +08:00
ZhanGSKen
cd7ed01216 添加Logon框和RSA加密模块。 2025-06-04 14:58:01 +08:00
ZhanGSKen
bb24bbfbd1 <appbase>APK 15.8.6 release Publish. 2025-06-04 13:17:34 +08:00
ZhanGSKen
2ba2f88510 添加调试模块 2025-06-04 13:16:51 +08:00
ZhanGSKen
db3a3644a8 <appbase>APK 15.8.5 release Publish. 2025-06-04 12:14:48 +08:00
ZhanGSKen
556bfa7024 添加主机识别 2025-06-04 12:11:19 +08:00
ZhanGSKen
4842a1ec30 添加云宝云测试接口 2025-06-04 11:21:26 +08:00
22 changed files with 1258 additions and 38 deletions

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue Jun 03 13:40:08 HKT 2025
stageCount=5
#Thu Jun 05 08:59:45 GMT 2025
stageCount=8
libraryProject=libappbase
baseVersion=15.8
publishVersion=15.8.4
buildCount=0
baseBetaVersion=15.8.5
publishVersion=15.8.7
buildCount=14
baseBetaVersion=15.8.8

View File

@@ -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">
<activity
android:name=".MainActivity"
@@ -39,7 +40,8 @@
android:resizeableActivity="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"/>
<activity android:name=".activities.New2Activity"
<activity
android:name=".activities.New2Activity"
android:label="New2Activity"
android:exported="true"
android:resizeableActivity="true"
@@ -74,7 +76,8 @@
<service android:name=".services.AssistantService"/>
<receiver android:name="cc.winboll.studio.appbase.receivers.MainReceiver"
<receiver
android:name="cc.winboll.studio.appbase.receivers.MainReceiver"
android:exported="true">
<intent-filter>
@@ -105,7 +108,8 @@
</receiver>
<receiver android:name=".receivers.APPNewsWidgetClickListener"
<receiver
android:name=".receivers.APPNewsWidgetClickListener"
android:exported="true">
<intent-filter>
@@ -122,7 +126,6 @@
android:name="android.max_aspect"
android:value="4.0"/>
</application>
</manifest>

View File

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

View File

@@ -5,6 +5,14 @@
android:id="@+id/item_home"
android:title="HOME"
android:icon="@drawable/ic_winboll"/>
<item
android:id="@+id/item_yun"
android:title="YUN"
android:icon="@drawable/ic_winboll"/>
<item
android:id="@+id/item_logon"
android:title="Logon"
android:icon="@drawable/ic_winboll"/>
<item
android:id="@+id/item_log"
android:title="LOG"

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- 允许访问 winboll.cc 及其子域名(原配置) -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">winboll.cc</domain>
</domain-config>
<!-- **新增:允许访问 IP 地址 10.8.0.250** -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">10.8.0.250</domain> <!-- 不包含子域名 -->
</domain-config>
</network-security-config>

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sat Jun 14 17:58:55 HKT 2025
stageCount=5
#Tue May 20 13:02:18 HKT 2025
stageCount=3
libraryProject=
baseVersion=15.3
publishVersion=15.3.4
publishVersion=15.3.2
buildCount=0
baseBetaVersion=15.3.5
baseBetaVersion=15.3.3

View File

@@ -145,14 +145,6 @@ public class Rules {
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
}
// 检验拨不通号码群
if (!isDefend && MainService.isPhoneInBoBullToon(phoneNumber)) {
LogUtils.d(TAG, String.format("PhoneNumber %s\n Is In BoBullToon", phoneNumber));
isDefend = true;
isConnect = false;
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
}
// 查询通讯录是否有该联系人
boolean isPhoneInContacts = ContactUtils.getInstance(mContext).isPhoneInContacts(mContext, phoneNumber);
if (!isDefend) {
@@ -166,6 +158,14 @@ public class Rules {
}
}
// 检验拨不通号码群
if (!isDefend && MainService.isPhoneInBoBullToon(phoneNumber)) {
LogUtils.d(TAG, String.format("PhoneNumber %s\n Is In BoBullToon", phoneNumber));
isDefend = true;
isConnect = false;
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
}
// 正则匹配规则名单校验
if (!isDefend) {
for (int i = 0; i < _PhoneConnectRuleModelList.size(); i++) {

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue Jun 03 13:40:01 HKT 2025
stageCount=5
#Thu Jun 05 08:59:45 GMT 2025
stageCount=8
libraryProject=libappbase
baseVersion=15.8
publishVersion=15.8.4
buildCount=0
baseBetaVersion=15.8.5
publishVersion=15.8.7
buildCount=14
baseBetaVersion=15.8.8

View File

@@ -103,6 +103,10 @@
</receiver>
<activity android:name="cc.winboll.studio.libappbase.activities.YunActivity"/>
<activity android:name="cc.winboll.studio.libappbase.activities.LogonActivity"/>
</application>
</manifest>

View File

@@ -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<zhangsken@188.com>
* @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());
}
}
}

View File

@@ -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<zhangsken@188.com>
* @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 {
// 手动关闭 ResponseJava 7 不支持 try-with-resources
if (response != null && response.body() != null) {
response.body().close();
}
}
}
}

View File

@@ -0,0 +1,53 @@
package cc.winboll.studio.libappbase.models;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @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;
}
}

View File

@@ -0,0 +1,92 @@
package cc.winboll.studio.libappbase.models;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @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;
}
}

View File

@@ -0,0 +1,128 @@
package cc.winboll.studio.libappbase.utils;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @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();
}
}
}
}
}

View File

@@ -0,0 +1,222 @@
package cc.winboll.studio.libappbase.utils;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @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);
}
}

View File

@@ -0,0 +1,281 @@
package cc.winboll.studio.libappbase.utils;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @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);
}
}

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:gravity="right"
android:padding="10dp"
android:id="@+id/ll_hostbar">
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="10.8.0.250:456"
android:id="@+id/rb_debughost"
android:onClick="onSwitchHost"/>
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="yun.winboll.cc"
android:id="@+id/rb_yunhost"
android:onClick="onSwitchHost"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Test RSA"
android:onClick="onTestRSA"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Test Login"
android:onClick="onTestLogin"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0">
<cc.winboll.studio.libappbase.LogView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/logview"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:gravity="right"
android:padding="10dp"
android:id="@+id/ll_hostbar">
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="10.8.0.250:456"
android:id="@+id/rb_debughost"
android:onClick="onSwitchHost"/>
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="yun.winboll.cc"
android:id="@+id/rb_yunhost"
android:onClick="onSwitchHost"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:gravity="right">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TestYun"
android:onClick="onTestYun"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0">
<cc.winboll.studio.libappbase.LogView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/logview"/>
</LinearLayout>
</LinearLayout>