Compare commits
75 Commits
aes-v15.8.
...
9ece6778b7
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9ece6778b7 | ||
![]() |
e13c8e7af0 | ||
![]() |
a4988b5b68 | ||
![]() |
5507126f6b | ||
![]() |
d381c29452 | ||
![]() |
945eadb617 | ||
![]() |
c5bffc5eef | ||
![]() |
88597fe407 | ||
![]() |
53f985533a | ||
![]() |
a3950f13ad | ||
![]() |
c878e9dc02 | ||
![]() |
f2f7cab330 | ||
![]() |
c4e88e9593 | ||
![]() |
08d9d92ae4 | ||
![]() |
74841c08dc | ||
![]() |
945bacb825 | ||
![]() |
0e464495fd | ||
![]() |
e8682ce410 | ||
![]() |
2e4003dae0 | ||
![]() |
198b0975ce | ||
![]() |
24a578a9d2 | ||
![]() |
46de24447f | ||
![]() |
1320984829 | ||
![]() |
abf1e5ba42 | ||
![]() |
1cd2f88038 | ||
![]() |
3f6e583d68 | ||
![]() |
271456bfcd | ||
![]() |
ee5458d82c | ||
![]() |
3a83367f71 | ||
![]() |
74b9350a6a | ||
![]() |
d2858f23f7 | ||
![]() |
40a5b9c339 | ||
![]() |
fd79113572 | ||
![]() |
9b911b583c | ||
![]() |
37817c3e8c | ||
![]() |
0b5402f5f3 | ||
![]() |
bea22e3853 | ||
![]() |
7e2ad0c01d | ||
![]() |
476ce02fc8 | ||
![]() |
bc697279ad | ||
![]() |
dee01f1179 | ||
![]() |
a500decc7a | ||
![]() |
5099d00050 | ||
![]() |
515d14e896 | ||
![]() |
f630e27ed8 | ||
![]() |
cd7ed01216 | ||
![]() |
bb24bbfbd1 | ||
![]() |
2ba2f88510 | ||
![]() |
db3a3644a8 | ||
![]() |
556bfa7024 | ||
![]() |
4842a1ec30 | ||
![]() |
b0388a2972 | ||
![]() |
bd5a1f18ce | ||
![]() |
99798b4816 | ||
![]() |
f93b6047a8 | ||
![]() |
daa3f858a0 | ||
![]() |
3fded32426 | ||
![]() |
8f85006040 | ||
![]() |
e28b0bd75e | ||
![]() |
77e98bafe4 | ||
![]() |
ff14d0c0c3 | ||
![]() |
950be3a182 | ||
![]() |
1f20fca9be | ||
![]() |
9a0ee889ba | ||
![]() |
c40066ca4d | ||
![]() |
a5083cc52f | ||
![]() |
6cce9c4d3f | ||
![]() |
df18c34976 | ||
![]() |
22ca83b5b7 | ||
![]() |
98233ce148 | ||
![]() |
b61c63c426 | ||
![]() |
f02dc215ca | ||
![]() |
1c27d0ccdc | ||
![]() |
803745d12e | ||
![]() |
a66be9cd37 |
@@ -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
|
||||
|
@@ -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,并取消相应项目模块的注释。
|
||||
## ★. 项目 Android SDK 编译环境设置(可选),local.properties-demo 要复制为 local.properties,并按需要设置 Android SDK 目录。
|
||||
## ★. 应用签名密钥 keystore 设置问题。一般调试编译只需用【Termux】cd 进 GenKeyStore 目录执行 $ bash gen_debug_keystore.sh 命令即可完成设置。
|
||||
## ☆. 应用 WiBoLL 签名密钥配置问题<非必须考虑>。设置时需要 clone 【keystore】模块源码并拷贝模块目录的 appkey.jks 与 appkey.keystore 到项目根目录即可。
|
||||
## ☆. 类库型模块编译环境设置(可选),winboll.properties-demo 要复制为 winboll.properties,并按需要设置 WinBoLL Maven 库登录用户信息。
|
||||
|
||||
|
||||
# ☆类库型项目编译方法
|
||||
|
@@ -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}")
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Tue Jun 03 19:16:41 HKT 2025
|
||||
#Thu Jun 19 20:42:40 HKT 2025
|
||||
stageCount=2
|
||||
libraryProject=libaes
|
||||
baseVersion=15.8
|
||||
publishVersion=15.8.1
|
||||
baseVersion=15.9
|
||||
publishVersion=15.9.1
|
||||
buildCount=0
|
||||
baseBetaVersion=15.8.2
|
||||
baseBetaVersion=15.9.2
|
||||
|
@@ -67,6 +67,6 @@ dependencies {
|
||||
// https://mvnrepository.com/artifact/com.android.support/recyclerview-v7
|
||||
api 'com.android.support:recyclerview-v7:28.0.0'
|
||||
|
||||
api 'cc.winboll.studio:libapputils:15.8.1'
|
||||
api 'cc.winboll.studio:libappbase:15.8.1'
|
||||
api 'cc.winboll.studio:libapputils:15.8.2'
|
||||
api 'cc.winboll.studio:libappbase:15.8.2'
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Mon May 19 21:45:28 GMT 2025
|
||||
#Sun Jun 01 08:02:46 GMT 2025
|
||||
stageCount=0
|
||||
libraryProject=
|
||||
baseVersion=15.0
|
||||
publishVersion=15.0.0
|
||||
buildCount=25
|
||||
buildCount=27
|
||||
baseBetaVersion=15.0.1
|
||||
|
@@ -68,6 +68,6 @@ dependencies {
|
||||
//api 'androidx.fragment:fragment:1.1.0'
|
||||
|
||||
api 'cc.winboll.studio:libaes:15.8.0'
|
||||
api 'cc.winboll.studio:libapputils:15.8.1'
|
||||
api 'cc.winboll.studio:libappbase:15.8.1'
|
||||
api 'cc.winboll.studio:libapputils:15.8.2'
|
||||
api 'cc.winboll.studio:libappbase:15.8.2'
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Mon May 19 21:43:40 GMT 2025
|
||||
#Thu Jun 19 12:49:47 GMT 2025
|
||||
stageCount=0
|
||||
libraryProject=
|
||||
baseVersion=15.0
|
||||
publishVersion=15.0.0
|
||||
buildCount=22
|
||||
buildCount=26
|
||||
baseBetaVersion=15.0.1
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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"
|
||||
|
12
appbase/src/main/res/xml/network_security_config.xml
Normal 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>
|
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sun Jun 01 16:01:35 HKT 2025
|
||||
stageCount=3
|
||||
#Tue Jun 03 15:05:48 HKT 2025
|
||||
stageCount=5
|
||||
libraryProject=libapputils
|
||||
baseVersion=15.8
|
||||
publishVersion=15.8.2
|
||||
publishVersion=15.8.4
|
||||
buildCount=0
|
||||
baseBetaVersion=15.8.3
|
||||
baseBetaVersion=15.8.5
|
||||
|
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sun May 04 05:32:00 GMT 2025
|
||||
stageCount=1
|
||||
#Tue Jun 24 09:54:47 HKT 2025
|
||||
stageCount=3
|
||||
libraryProject=
|
||||
baseVersion=15.2
|
||||
publishVersion=15.2.0
|
||||
buildCount=74
|
||||
baseBetaVersion=15.2.1
|
||||
publishVersion=15.2.2
|
||||
buildCount=0
|
||||
baseBetaVersion=15.2.3
|
||||
|
@@ -42,23 +42,24 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
/*compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
api project(':libjc')
|
||||
api 'cc.winboll.studio:libaes:15.9.1'
|
||||
api 'cc.winboll.studio:libapputils:15.8.4'
|
||||
api 'cc.winboll.studio:libappbase:15.8.4'
|
||||
|
||||
// https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on
|
||||
//implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15to18:1.69'
|
||||
implementation 'org.bouncycastle:bcpkix-jdk15to18:1.69'
|
||||
|
||||
api project(':libjc')
|
||||
api 'androidx.appcompat:appcompat:1.0.0'
|
||||
api 'com.google.android.material:material:1.0.0'
|
||||
|
||||
api 'cc.winboll.studio:libapputils:9.1.0'
|
||||
api 'cc.winboll.studio:libappbase:1.0.3'
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Fri Jan 10 22:03:57 GMT 2025
|
||||
#Tue Jun 24 11:17:30 GMT 2025
|
||||
stageCount=0
|
||||
libraryProject=libjc
|
||||
baseVersion=1.0
|
||||
publishVersion=1.0.0
|
||||
buildCount=133
|
||||
buildCount=135
|
||||
baseBetaVersion=1.0.1
|
||||
|
@@ -15,10 +15,10 @@ import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import cc.winboll.studio.jc.R;
|
||||
import cc.winboll.studio.libapputils.log.LogUtils;
|
||||
import cc.winboll.studio.libjc.JAR_RUNNING_MODE;
|
||||
import cc.winboll.studio.libjc.JCMainThread;
|
||||
import cc.winboll.studio.libjc.net.JCSocketClient;
|
||||
import cc.winboll.studio.libjc.util.LogUtils;
|
||||
import cc.winboll.studio.libjc.Main;
|
||||
|
||||
final public class MainActivity extends Activity implements JCMainThread.OnMessageListener {
|
||||
|
||||
@@ -77,7 +77,7 @@ final public class MainActivity extends Activity implements JCMainThread.OnMessa
|
||||
// 启动主线程
|
||||
_JCMainThread = JCMainThread.getInstance(getPackageName());
|
||||
_JCMainThread.setOnLogListener(this);
|
||||
_JCMainThread.setRunningMode(JAR_RUNNING_MODE.JC);
|
||||
//_JCMainThread.setRunningMode(Main.JAR_RUNNING_MODE.JC);
|
||||
_JCMainThread.start();
|
||||
|
||||
// 设置 WinBoll 应用 UI 类型
|
||||
|
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Tue Jun 03 19:16:41 HKT 2025
|
||||
#Thu Jun 19 20:42:26 HKT 2025
|
||||
stageCount=2
|
||||
libraryProject=libaes
|
||||
baseVersion=15.8
|
||||
publishVersion=15.8.1
|
||||
baseVersion=15.9
|
||||
publishVersion=15.9.1
|
||||
buildCount=0
|
||||
baseBetaVersion=15.8.2
|
||||
baseBetaVersion=15.9.2
|
||||
|
@@ -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'
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -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 {
|
||||
// 手动关闭 Response(Java 7 不支持 try-with-resources)
|
||||
if (response != null && response.body() != null) {
|
||||
response.body().close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
68
libappbase/src/main/res/layout/activity_logon.xml
Normal 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>
|
||||
|
63
libappbase/src/main/res/layout/activity_yun.xml
Normal 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>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sun Jun 01 16:01:19 HKT 2025
|
||||
stageCount=3
|
||||
#Tue Jun 03 15:05:42 HKT 2025
|
||||
stageCount=5
|
||||
libraryProject=libapputils
|
||||
baseVersion=15.8
|
||||
publishVersion=15.8.2
|
||||
publishVersion=15.8.4
|
||||
buildCount=0
|
||||
baseBetaVersion=15.8.3
|
||||
baseBetaVersion=15.8.5
|
||||
|
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Fri Jan 10 22:03:57 GMT 2025
|
||||
#Tue Jun 24 11:17:30 GMT 2025
|
||||
stageCount=0
|
||||
libraryProject=libjc
|
||||
baseVersion=1.0
|
||||
publishVersion=1.0.0
|
||||
buildCount=133
|
||||
buildCount=135
|
||||
baseBetaVersion=1.0.1
|
||||
|
@@ -21,7 +21,7 @@ public class Main {
|
||||
public final static int JAR_RUNNING_MODE_JCNDK_DEBUG = 4;
|
||||
public final static int JAR_RUNNING_MODE_JC = 5;
|
||||
public final static int JAR_RUNNING_MODE_JC_DEBUG = 6;
|
||||
public enum JAR_RUNNING_MODE {
|
||||
public static enum JAR_RUNNING_MODE {
|
||||
UNKNOWN(JAR_RUNNING_MODE_UNKNOWN),
|
||||
CONSOLE(JAR_RUNNING_MODE_CONSOLE),
|
||||
CONSOLE_DEBUG(JAR_RUNNING_MODE_CONSOLE_DEBUG),
|
||||
|
@@ -29,7 +29,7 @@ android {
|
||||
// versionName 更新后需要手动设置
|
||||
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
|
||||
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||
versionName "15.2"
|
||||
versionName "15.3"
|
||||
if(true) {
|
||||
versionName = genVersionName("${versionName}")
|
||||
}
|
||||
@@ -45,15 +45,17 @@ android {
|
||||
|
||||
dependencies {
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
api 'cc.winboll.studio:libaes:15.8.0'
|
||||
api 'cc.winboll.studio:libapputils:15.8.1'
|
||||
api 'cc.winboll.studio:libappbase:15.8.1'
|
||||
api 'cc.winboll.studio:libaes:15.9.2'
|
||||
api 'cc.winboll.studio:libapputils:15.8.4'
|
||||
api 'cc.winboll.studio:libappbase:15.8.4'
|
||||
|
||||
api 'io.github.medyo:android-about-page:2.0.0'
|
||||
api 'com.github.getActivity:ToastUtils:10.5'
|
||||
api 'com.jcraft:jsch:0.1.55'
|
||||
api 'org.jsoup:jsoup:1.13.1'
|
||||
api 'com.squareup.okhttp3:okhttp:4.4.1'
|
||||
|
||||
api 'com.belerweb:pinyin4j:2.5.1'
|
||||
|
||||
// 权限请求框架:https://github.com/getActivity/XXPermissions
|
||||
api 'com.github.getActivity:XXPermissions:18.63'
|
||||
|
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Tue May 20 20:39:06 HKT 2025
|
||||
stageCount=6
|
||||
#Thu Aug 28 21:13:38 HKT 2025
|
||||
stageCount=4
|
||||
libraryProject=
|
||||
baseVersion=15.2
|
||||
publishVersion=15.2.5
|
||||
baseVersion=15.3
|
||||
publishVersion=15.3.3
|
||||
buildCount=0
|
||||
baseBetaVersion=15.2.6
|
||||
baseBetaVersion=15.3.4
|
||||
|
@@ -17,7 +17,7 @@ import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
|
||||
import cc.winboll.studio.mymessagemanager.App;
|
||||
import cc.winboll.studio.mymessagemanager.R;
|
||||
|
||||
public class AboutActivity extends WinBollActivity implements IWinBoLLActivity {
|
||||
public class AboutActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
|
||||
public static final String TAG = "AboutActivity";
|
||||
|
||||
@@ -79,11 +79,11 @@ public class AboutActivity extends WinBollActivity implements IWinBoLLActivity {
|
||||
appInfo.setAppName(getString(R.string.app_name));
|
||||
appInfo.setAppIcon(cc.winboll.studio.libaes.R.drawable.ic_winboll);
|
||||
appInfo.setAppDescription(getString(R.string.app_description));
|
||||
appInfo.setAppGitName("APP");
|
||||
appInfo.setAppGitName("APPBase");
|
||||
appInfo.setAppGitOwner("Studio");
|
||||
appInfo.setAppGitAPPBranch(szBranchName);
|
||||
appInfo.setAppGitAPPSubProjectFolder(szBranchName);
|
||||
appInfo.setAppHomePage("https://www.winboll.cc/studio/details.php?app=MyMessageManager");
|
||||
appInfo.setAppHomePage("https://discuz.winboll.cc/forum.php?mod=viewthread&tid=5&extra=page%3D1");
|
||||
appInfo.setAppAPKName("MyMessageManager");
|
||||
appInfo.setAppAPKFolderName("MyMessageManager");
|
||||
return new AboutView(mContext, appInfo);
|
||||
|
@@ -103,6 +103,9 @@ abstract public class BaseActivity extends AppCompatActivity {
|
||||
} else if (R.id.item_defaulttheme == item.getItemId()) {
|
||||
AESThemeUtil.saveThemeStyleID(this, R.style.MyAppTheme);
|
||||
recreate();
|
||||
} else if (R.id.item_defaulttheme == item.getItemId()) {
|
||||
AESThemeUtil.saveThemeStyleID(this, R.style.MyAppTheme);
|
||||
recreate();
|
||||
}
|
||||
//ToastUtils.show("nThemeStyleID " + Integer.toString(nThemeStyleID));
|
||||
|
||||
|
@@ -35,6 +35,8 @@ public class ComposeSMSActivity extends BaseActivity {
|
||||
String mszSMSBody;
|
||||
String mszScheme;
|
||||
String mszPhoneTo;
|
||||
TextView mtvTOName;
|
||||
EditText metTONameSearch;
|
||||
EditText metTO;
|
||||
EditText metSMSBody;
|
||||
SimpleAdapter mSimpleAdapter;
|
||||
@@ -59,9 +61,9 @@ public class ComposeSMSActivity extends BaseActivity {
|
||||
// 初始化视图
|
||||
initView();
|
||||
// 设置适配器
|
||||
initAdapter();
|
||||
initAdapter(null);
|
||||
// 设置搜索到的匹配位置
|
||||
setListViewPrePosition();
|
||||
setListViewPrePositionByPhone();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -76,8 +78,25 @@ public class ComposeSMSActivity extends BaseActivity {
|
||||
setActionBar(mToolbar);
|
||||
|
||||
// 初始化联系人栏目框
|
||||
mtvTOName = findViewById(R.id.activitycomposesmsTextView2);
|
||||
mrlContracts = findViewById(R.id.activitycomposesmsRelativeLayout1);
|
||||
//mrlContracts.setBackground(drawableFrame);
|
||||
metTONameSearch = findViewById(R.id.activitycomposesmsEditText2);
|
||||
metTONameSearch.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
metTO.setText("");
|
||||
setListViewPrePositionByName();
|
||||
}
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化联系人列表
|
||||
mlvContracts = findViewById(R.id.activitycomposesmsListView1);
|
||||
@@ -88,7 +107,10 @@ public class ComposeSMSActivity extends BaseActivity {
|
||||
metTO.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
setListViewPrePosition();
|
||||
mtvTOName.setText("");
|
||||
//重新加载数据
|
||||
initAdapter(null);
|
||||
setListViewPrePositionByPhone();
|
||||
}
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
@@ -96,6 +118,7 @@ public class ComposeSMSActivity extends BaseActivity {
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
@@ -136,18 +159,34 @@ public class ComposeSMSActivity extends BaseActivity {
|
||||
}
|
||||
|
||||
//
|
||||
// 设置搜索到的匹配位置
|
||||
// 设置号码搜索到的匹配位置
|
||||
//
|
||||
void setListViewPrePosition() {
|
||||
int nPrePosition = getContractsDataPrePosition(metTO.getText().toString());
|
||||
void setListViewPrePositionByPhone() {
|
||||
int nPrePosition = getContractsDataPrePositionByPhone(metTO.getText().toString());
|
||||
|
||||
PhoneUtil phoneUtils = new PhoneUtil(this);
|
||||
mtvTOName.setText(phoneUtils.getNameByPhone(metTO.getText().toString()));
|
||||
|
||||
mlvContracts.setSelected(false);
|
||||
mlvContracts.setSelection(nPrePosition);
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// 设置名称搜索到的匹配位置
|
||||
//
|
||||
void setListViewPrePositionByName() {
|
||||
PhoneUtil phoneUtils = new PhoneUtil(this);
|
||||
List<PhoneBean> newPhoneData = phoneUtils.getPhonesByName(metTONameSearch.getText().toString());
|
||||
// 重新绑定数据
|
||||
initAdapter(newPhoneData);
|
||||
mlvContracts.setSelected(false);
|
||||
}
|
||||
|
||||
//
|
||||
// 返回搜索到的匹配位置
|
||||
//
|
||||
int getContractsDataPrePosition(String szPhone) {
|
||||
int getContractsDataPrePositionByPhone(String szPhone) {
|
||||
for (int i = 0; i < mListPhoneBeanContracts.size(); i++) {
|
||||
if (mListPhoneBeanContracts.get(i).getTelPhone().compareTo(szPhone) > -1) {
|
||||
return i;
|
||||
@@ -157,15 +196,65 @@ public class ComposeSMSActivity extends BaseActivity {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// 返回搜索到的匹配位置
|
||||
//
|
||||
int getContractsDataPrePositionByName(String szName) {
|
||||
for (int i = 0; i < mListPhoneBeanContracts.size(); i++) {
|
||||
if (mListPhoneBeanContracts.get(i).getName().startsWith(szName)) {
|
||||
return i;
|
||||
}
|
||||
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
//
|
||||
// 初始化适配器
|
||||
//
|
||||
void initAdapter() {
|
||||
/*void initAdapter() {
|
||||
// 初始化联系人数据适配器
|
||||
mAdapterData = new ArrayList<>();
|
||||
// 读取联系人数据
|
||||
final PhoneUtil phoneUtils = new PhoneUtil(this);
|
||||
mListPhoneBeanContracts = phoneUtils.getPhoneList();
|
||||
// 映射联系人数据给适配器数据对象
|
||||
for (int i = 0;i < mListPhoneBeanContracts.size();i++) {
|
||||
Map<String,Object> map =new HashMap<>();
|
||||
map.put(MAP_NAME, mListPhoneBeanContracts.get(i).getName());
|
||||
map.put(MAP_PHONE, mListPhoneBeanContracts.get(i).getTelPhone());
|
||||
mAdapterData.add(map);
|
||||
}
|
||||
// 绑定适配器与数据
|
||||
mSimpleAdapter = new SimpleAdapter(ComposeSMSActivity.this, mAdapterData, R.layout.listview_contracts
|
||||
, new String[]{MAP_NAME, MAP_PHONE}
|
||||
, new int[]{R.id.listviewcontractsTextView1, R.id.listviewcontractsTextView2});
|
||||
mSimpleAdapter.setDropDownViewResource(R.layout.listview_contracts);
|
||||
mlvContracts.setAdapter(mSimpleAdapter);
|
||||
mlvContracts.setOnItemClickListener(new ListView.OnItemClickListener() {
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
metTO.setText(mAdapterData.get(position).get(MAP_PHONE).toString());
|
||||
mListPhoneBeanContracts = phoneUtils.getPhoneList();
|
||||
mtvTOName.setText(phoneUtils.getNameByPhone(metTO.getText().toString()));
|
||||
|
||||
}
|
||||
});
|
||||
}*/
|
||||
|
||||
void initAdapter(List<PhoneBean> initData) {
|
||||
// 初始化联系人数据适配器
|
||||
mAdapterData = new ArrayList<>();
|
||||
// 读取联系人数据
|
||||
PhoneUtil phoneUtils = new PhoneUtil(this);
|
||||
mListPhoneBeanContracts = phoneUtils.getPhoneList();
|
||||
mAdapterData = new ArrayList<>();
|
||||
final PhoneUtil phoneUtils = new PhoneUtil(this);
|
||||
if (initData != null) {
|
||||
mListPhoneBeanContracts = initData;
|
||||
} else {
|
||||
// 读取联系人数据
|
||||
mListPhoneBeanContracts = phoneUtils.getPhoneList();
|
||||
}
|
||||
|
||||
// 映射联系人数据给适配器数据对象
|
||||
for (int i = 0;i < mListPhoneBeanContracts.size();i++) {
|
||||
Map<String,Object> map =new HashMap<>();
|
||||
@@ -184,7 +273,7 @@ public class ComposeSMSActivity extends BaseActivity {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
metTO.setText(mAdapterData.get(position).get(MAP_PHONE).toString());
|
||||
|
||||
mtvTOName.setText(phoneUtils.getNameByPhone(metTO.getText().toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -128,7 +128,7 @@ public class MainActivity extends BaseActivity {
|
||||
mToolbar = findViewById(R.id.activitymainASupportToolbar1);
|
||||
mToolbar.setSubtitle(getString(R.string.activity_name_main));
|
||||
setSupportActionBar(mToolbar);
|
||||
|
||||
|
||||
boolean isEnableService = mAppConfigUtil.mAppConfigBean.isEnableService();
|
||||
msvEnableService = findViewById(R.id.activitymainSwitchView1);
|
||||
msvEnableService.setChecked(isEnableService);
|
||||
@@ -269,17 +269,9 @@ public class MainActivity extends BaseActivity {
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
//return super.onCreateOptionsMenu(menu);
|
||||
getMenuInflater().inflate(R.menu.toolbar_main, menu);
|
||||
|
||||
/*ThemeUtil.BaseTheme baseTheme = ThemeUtil.getTheme(mAppConfigUtil.mAppConfigBean.getAppThemeID());
|
||||
if (baseTheme == ThemeUtil.BaseTheme.DEFAULT) {
|
||||
menu.findItem(R.id.app_defaulttheme).setChecked(true);
|
||||
} else if (baseTheme == ThemeUtil.BaseTheme.SKY) {
|
||||
menu.findItem(R.id.app_skytheme).setChecked(true);
|
||||
} else if (baseTheme == ThemeUtil.BaseTheme.GOLDEN) {
|
||||
menu.findItem(R.id.app_goldentheme).setChecked(true);
|
||||
}*/
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
super.onCreateOptionsMenu(menu);
|
||||
getMenuInflater().inflate(R.menu.toolbar_main2, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void reloadSMS() {
|
||||
@@ -306,7 +298,7 @@ public class MainActivity extends BaseActivity {
|
||||
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(i);
|
||||
} else if (nItemId == R.id.app_log) {
|
||||
//App.getWinBoLLActivityManager().startLogActivity(this);
|
||||
App.getWinBoLLActivityManager().startLogActivity(this);
|
||||
} else if (nItemId == R.id.app_unittest) {
|
||||
Intent i = new Intent(MainActivity.this, UnitTestActivity.class);
|
||||
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
@@ -324,7 +316,7 @@ public class MainActivity extends BaseActivity {
|
||||
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(i);
|
||||
}
|
||||
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
|
@@ -4,11 +4,16 @@ import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
@@ -16,19 +21,17 @@ import android.widget.Toolbar;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
|
||||
import cc.winboll.studio.mymessagemanager.R;
|
||||
import cc.winboll.studio.mymessagemanager.activitys.SMSActivity;
|
||||
import cc.winboll.studio.mymessagemanager.adapters.SMSArrayAdapter;
|
||||
import cc.winboll.studio.mymessagemanager.utils.AddressUtils;
|
||||
import cc.winboll.studio.mymessagemanager.utils.SMSUtil;
|
||||
import cc.winboll.studio.mymessagemanager.utils.ViewUtil;
|
||||
import cc.winboll.studio.mymessagemanager.views.BottomPositionFixedScrollView;
|
||||
import cc.winboll.studio.mymessagemanager.views.SMSListViewForScrollView;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public class SMSActivity extends BaseActivity {
|
||||
public static String TAG = "SMSActivity";
|
||||
|
||||
public static final String ACTION_NOTIFY_SMS_CHANGED = "cc.winboll.studio.mymessagemanager.activitys.SMSActivity.ACTION_NOTIFY_SMS_CHANGED";
|
||||
|
||||
public static final String EXTRA_PHONE = "Phone";
|
||||
final static int MSG_SET_FOCUS = 0;
|
||||
|
||||
@@ -36,10 +39,11 @@ public class SMSActivity extends BaseActivity {
|
||||
Toolbar mToolbar;
|
||||
String mszPhoneTo;
|
||||
SMSArrayAdapter mSMSArrayAdapter;
|
||||
ScrollView mScrollView;
|
||||
BottomPositionFixedScrollView mScrollView1;
|
||||
EditText metSMSBody;
|
||||
SMSActivityBroadcastReceiver mSMSActivityBroadcastReceiver;
|
||||
Handler mSetFocusHandler;
|
||||
private boolean isImeVisible = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -47,45 +51,90 @@ public class SMSActivity extends BaseActivity {
|
||||
setContentView(R.layout.activity_sms);
|
||||
|
||||
initView();
|
||||
mSetFocusHandler = new MyHandler(SMSActivity.this);
|
||||
scrollScrollView();
|
||||
setupImeStatusListener();
|
||||
|
||||
// 每隔一定时间设置输入框获得焦点
|
||||
//
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
while (true) {
|
||||
try {
|
||||
Thread.sleep(1500);
|
||||
} catch (InterruptedException e) {}
|
||||
Message message = mSetFocusHandler.obtainMessage(MSG_SET_FOCUS);
|
||||
mSetFocusHandler.sendMessage(message);
|
||||
}
|
||||
}}.start();
|
||||
// 新增:监听窗口加载完成,触发mScrollView1滚动到底部
|
||||
setupScrollToBottomAfterWindowLoaded();
|
||||
}
|
||||
|
||||
//
|
||||
// 设置输入框获得焦点的类
|
||||
//
|
||||
static class MyHandler extends Handler {
|
||||
WeakReference<SMSActivity> mActivity;
|
||||
MyHandler(SMSActivity activity) {
|
||||
mActivity = new WeakReference<SMSActivity>(activity);
|
||||
}
|
||||
public void handleMessage(Message msg) {
|
||||
SMSActivity theActivity = mActivity.get();
|
||||
switch (msg.what) {
|
||||
case MSG_SET_FOCUS:
|
||||
theActivity.metSMSBody.setFocusable(true);
|
||||
theActivity.metSMSBody.requestFocus();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
super.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
// 新增:窗口加载完成后让mScrollView1滚动到底部
|
||||
private void setupScrollToBottomAfterWindowLoaded() {
|
||||
final View rootView = findViewById(android.R.id.content);
|
||||
// 监听根布局绘制完成(窗口加载完成的标志)
|
||||
rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
// 滚动到底部
|
||||
mScrollView1.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mScrollView1.fullScroll(ScrollView.FOCUS_DOWN);
|
||||
}
|
||||
});
|
||||
|
||||
// 移除监听,避免重复触发
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
rootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
} else {
|
||||
rootView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupImeStatusListener() {
|
||||
final View rootView = findViewById(android.R.id.content);
|
||||
rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
int rootViewHeight = rootView.getHeight();
|
||||
int screenHeight = getResources().getDisplayMetrics().heightPixels;
|
||||
int imeThreshold = dp2px(200);
|
||||
|
||||
boolean currentImeVisible = (screenHeight - rootViewHeight) > imeThreshold;
|
||||
|
||||
if (currentImeVisible != isImeVisible) {
|
||||
isImeVisible = currentImeVisible;
|
||||
setupScrollView1Height();
|
||||
if (!isImeVisible) {
|
||||
metSMSBody.clearFocus();
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
rootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
} else {
|
||||
rootView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||
}
|
||||
setupImeStatusListener();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private int dp2px(int dp) {
|
||||
return (int) (dp * getResources().getDisplayMetrics().density + 0.5f);
|
||||
}
|
||||
|
||||
/*static class MyHandler extends Handler {
|
||||
WeakReference<SMSActivity> mActivity;
|
||||
MyHandler(SMSActivity activity) {
|
||||
mActivity = new WeakReference<SMSActivity>(activity);
|
||||
}
|
||||
public void handleMessage(Message msg) {
|
||||
SMSActivity theActivity = mActivity.get();
|
||||
switch (msg.what) {
|
||||
case MSG_SET_FOCUS:
|
||||
theActivity.metSMSBody.setFocusable(true);
|
||||
theActivity.metSMSBody.requestFocus();
|
||||
theActivity.setupScrollView1Height();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
super.handleMessage(msg);
|
||||
}
|
||||
}*/
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
@@ -94,135 +143,130 @@ public class SMSActivity extends BaseActivity {
|
||||
}
|
||||
|
||||
void initView() {
|
||||
// 发送端空号码退出
|
||||
mszPhoneTo = getIntent().getStringExtra(EXTRA_PHONE);
|
||||
if (mszPhoneTo == null || mszPhoneTo.trim().equals("")) {
|
||||
finish();
|
||||
}
|
||||
|
||||
// 初始化标题栏
|
||||
mToolbar = findViewById(R.id.activitysmsASupportToolbar1);
|
||||
mToolbar = (Toolbar) findViewById(R.id.activitysmsASupportToolbar1);
|
||||
mToolbar.setSubtitle(getString(R.string.activity_name_smsinphone) + " < Phone : " + AddressUtils.getFormattedAddress(mszPhoneTo) + " >");
|
||||
setActionBar(mToolbar);
|
||||
|
||||
// 初始化滚动窗口
|
||||
mScrollView = findViewById(R.id.activitysmsinphoneScrollView1);
|
||||
mScrollView1 = (BottomPositionFixedScrollView) findViewById(R.id.activitysmsScrollView1);
|
||||
|
||||
// 初始化发送消息框
|
||||
//Drawable drawableFrame = AppCompatResources.getDrawable(this, R.drawable.bg_frame);
|
||||
metSMSBody = findViewById(R.id.viewsmssendpart1EditText1);
|
||||
//metSMSBody.setBackground(drawableFrame);
|
||||
metSMSBody = (EditText) findViewById(R.id.viewsmssendpart1EditText1);
|
||||
metSMSBody.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
setupScrollView1Height();
|
||||
}
|
||||
});
|
||||
metSMSBody.setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
setupScrollView1Height();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化发送拉动控件
|
||||
final AOHPCTCSeekBar aOHPCTCSeekBar = findViewById(R.id.viewsmssendpart1AOHPCTCSeekBar1);
|
||||
final AOHPCTCSeekBar aOHPCTCSeekBar = (AOHPCTCSeekBar) findViewById(R.id.viewsmssendpart1AOHPCTCSeekBar1);
|
||||
aOHPCTCSeekBar.setThumb(getDrawable(R.drawable.ic_message));
|
||||
aOHPCTCSeekBar.setThumbOffset(20);
|
||||
aOHPCTCSeekBar.setOnOHPCListener(
|
||||
new AOHPCTCSeekBar.OnOHPCListener(){
|
||||
@Override
|
||||
public void onOHPCommit() {
|
||||
//Toast.makeText(getApplication(), "Send", Toast.LENGTH_SHORT).show();
|
||||
sendSMS();
|
||||
}
|
||||
});
|
||||
aOHPCTCSeekBar.setOnOHPCListener(new AOHPCTCSeekBar.OnOHPCListener() {
|
||||
@Override
|
||||
public void onOHPCommit() {
|
||||
sendSMS();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化提示框
|
||||
TextView tvAOHPCTCSeekBarMSG = findViewById(R.id.viewsmssendpart1TextView1);
|
||||
TextView tvAOHPCTCSeekBarMSG = (TextView) findViewById(R.id.viewsmssendpart1TextView1);
|
||||
tvAOHPCTCSeekBarMSG.setText(R.string.msg_100sendmsg);
|
||||
|
||||
mlvSMS = (SMSListViewForScrollView) findViewById(R.id.activitysmsinphoneListView1);
|
||||
|
||||
// 准备数据
|
||||
mlvSMS = (SMSListViewForScrollView) findViewById(R.id.activitysmsSMSListViewForScrollView1);
|
||||
mSMSArrayAdapter = new SMSArrayAdapter(SMSActivity.this, mszPhoneTo);
|
||||
mlvSMS.setAdapter(mSMSArrayAdapter);
|
||||
|
||||
// 设置短信列表滚动到底部就取消已发送的通知消息
|
||||
//
|
||||
mlvSMS.setOnScrollListener(new AbsListView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrollStateChanged(AbsListView view, int scrollState) {
|
||||
}
|
||||
@Override
|
||||
public void onScrollStateChanged(AbsListView view, int scrollState) {}
|
||||
|
||||
@Override
|
||||
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
|
||||
if (firstVisibleItem + visibleItemCount == totalItemCount && totalItemCount > 0) {
|
||||
// 滑动到了底部
|
||||
mSMSArrayAdapter.cancelMessageNotification();
|
||||
}
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
|
||||
if (firstVisibleItem + visibleItemCount == totalItemCount && totalItemCount > 0) {
|
||||
mSMSArrayAdapter.cancelMessageNotification();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mSMSActivityBroadcastReceiver = new SMSActivityBroadcastReceiver();
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(ACTION_NOTIFY_SMS_CHANGED);
|
||||
IntentFilter intentFilter = new IntentFilter(ACTION_NOTIFY_SMS_CHANGED);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mSMSActivityBroadcastReceiver, intentFilter);
|
||||
|
||||
/*SMSView mSMSView = findViewById(R.id.viewsmssendSMSView1);
|
||||
mSMSView.setSMSType(SMSView.SMSType.SEND);*/
|
||||
}
|
||||
|
||||
//
|
||||
// 更新信息列表
|
||||
//
|
||||
private void setupScrollView1Height() {
|
||||
mScrollView1.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final ScrollView scrollView2 = (ScrollView) findViewById(R.id.activitysmsScrollView2);
|
||||
final BottomPositionFixedScrollView scrollView1 = (BottomPositionFixedScrollView) findViewById(R.id.activitysmsScrollView1);
|
||||
final View includeView = findViewById(R.id.activitysmsinclude1);
|
||||
|
||||
scrollView2.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int scrollView2Height = scrollView2.getHeight();
|
||||
int includeHeight = includeView.getHeight();
|
||||
int targetHeight = Math.max(scrollView2Height - includeHeight, 0);
|
||||
|
||||
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) scrollView1.getLayoutParams();
|
||||
params.height = targetHeight;
|
||||
scrollView1.setLayoutParams(params);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
public void updateSMSView() {
|
||||
mSMSArrayAdapter.reLoadSMSList(SMSActivity.this, mszPhoneTo);
|
||||
mSMSArrayAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
//
|
||||
// 滚动消息文本框
|
||||
//
|
||||
void scrollScrollView() {
|
||||
|
||||
ViewUtil.scrollScrollView(mScrollView);
|
||||
|
||||
ViewUtil.scrollScrollView(mScrollView1);
|
||||
}
|
||||
|
||||
//
|
||||
// 发送短信
|
||||
//
|
||||
void sendSMS() {
|
||||
// 空消息不发送
|
||||
String szSMSBody = metSMSBody.getText().toString();
|
||||
if (szSMSBody.equals("")) {
|
||||
Toast.makeText(getApplication(), "没有消息内容可发送。", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送短信
|
||||
if (SMSUtil.sendMessageByInterface2(this, mszPhoneTo, szSMSBody)) {
|
||||
metSMSBody.setText("");
|
||||
new Handler().postDelayed(new Runnable(){
|
||||
@Override
|
||||
public void run() {
|
||||
updateSMSView();
|
||||
ViewUtil.scrollScrollView(mScrollView);
|
||||
}
|
||||
}, 1000);
|
||||
metSMSBody.clearFocus();
|
||||
new Handler().postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateSMSView();
|
||||
ViewUtil.scrollScrollView(mScrollView1);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
class SMSActivityBroadcastReceiver extends BroadcastReceiver {
|
||||
|
||||
public SMSActivityBroadcastReceiver() {
|
||||
//LogUtils.d(TAG, "SMSActivityBroadcastReceiver()");
|
||||
}
|
||||
public SMSActivityBroadcastReceiver() {}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
switch (intent.getAction()) {
|
||||
case ACTION_NOTIFY_SMS_CHANGED :
|
||||
//Toast.makeText(context, "ACTION_NOTIFY_SMS_CHANGED", Toast.LENGTH_SHORT).show();
|
||||
updateSMSView();
|
||||
ViewUtil.scrollScrollView(mScrollView);
|
||||
//LogUtils.d(TAG, "ACTION_NOTIFY_SMS_CHANGED");
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unexpected value: " + intent.getAction());
|
||||
if (ACTION_NOTIFY_SMS_CHANGED.equals(intent.getAction())) {
|
||||
updateSMSView();
|
||||
ViewUtil.scrollScrollView(mScrollView1);
|
||||
} else {
|
||||
throw new IllegalStateException("Unexpected value: " + intent.getAction());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@@ -13,9 +13,9 @@ import cc.winboll.studio.libaes.beans.AESThemeBean;
|
||||
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
||||
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
|
||||
|
||||
public class WinBollActivity extends AppCompatActivity implements IWinBoLLActivity {
|
||||
public class WinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
|
||||
|
||||
public static final String TAG = "WinBollActivity";
|
||||
public static final String TAG = "WinBoLLActivity";
|
||||
|
||||
protected volatile AESThemeBean.ThemeType mThemeType;
|
||||
|
@@ -5,6 +5,7 @@ package cc.winboll.studio.mymessagemanager.utils;
|
||||
* @Date 2024/07/19 14:30:57
|
||||
* @Describe 手机联系人工具类
|
||||
*/
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
@@ -12,6 +13,12 @@ import android.net.Uri;
|
||||
import android.provider.ContactsContract;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.mymessagemanager.beans.PhoneBean;
|
||||
import net.sourceforge.pinyin4j.PinyinHelper;
|
||||
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
|
||||
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
|
||||
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
|
||||
import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
@@ -38,28 +45,137 @@ public class PhoneUtil {
|
||||
}
|
||||
|
||||
// 读取所有联系人
|
||||
//
|
||||
public List<PhoneBean> getPhoneList() {
|
||||
List<PhoneBean> listPhoneBean = new ArrayList<>();
|
||||
ContentResolver cr = mContext.getContentResolver();
|
||||
Cursor cursor = cr.query(mUriPhoneContent, new String[]{NUMBER, DISPLAY_NAME}, null, null, null);
|
||||
while (cursor.moveToNext()) {
|
||||
PhoneBean phoneBean = new PhoneBean(cursor.getString(1), cursor.getString(0).replaceAll("\\s", ""));
|
||||
listPhoneBean.add(phoneBean);
|
||||
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
// 去除号码中的空格
|
||||
String phone = cursor.getString(0).replaceAll("\\s", "");
|
||||
String name = cursor.getString(1);
|
||||
PhoneBean phoneBean = new PhoneBean(name, phone);
|
||||
listPhoneBean.add(phoneBean);
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
cursor.close();
|
||||
|
||||
// 按电话号码排序
|
||||
Collections.sort(listPhoneBean, new Comparator<PhoneBean>() {
|
||||
@Override
|
||||
public int compare(PhoneBean o1, PhoneBean o2) {
|
||||
return o1.getTelPhone().compareTo(o2.getTelPhone());
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public int compare(PhoneBean o1, PhoneBean o2) {
|
||||
return o1.getTelPhone().compareTo(o2.getTelPhone());
|
||||
}
|
||||
});
|
||||
|
||||
return listPhoneBean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据联系人名称查询号码(兼容拼音查询)
|
||||
* @param keyword 搜索关键词(支持汉字、拼音、拼音首字母)
|
||||
* @return 匹配的联系人列表(包含姓名和号码)
|
||||
*/
|
||||
public List<PhoneBean> getPhonesByName(String keyword) {
|
||||
List<PhoneBean> result = new ArrayList<>();
|
||||
if (keyword == null || keyword.trim().isEmpty()) {
|
||||
return result; // 关键词为空,返回空列表
|
||||
}
|
||||
|
||||
// 获取所有联系人
|
||||
List<PhoneBean> allContacts = getPhoneList();
|
||||
// 统一转为小写,忽略大小写
|
||||
String keywordLower = keyword.trim().toLowerCase();
|
||||
|
||||
for (PhoneBean contact : allContacts) {
|
||||
String name = contact.getName();
|
||||
if (name == null || name.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 1. 直接匹配姓名(包含关键词)
|
||||
if (name.toLowerCase().contains(keywordLower)) {
|
||||
result.add(contact);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. 匹配姓名的全拼(包含关键词)
|
||||
String namePinyin = getPinyin(name).toLowerCase();
|
||||
if (namePinyin.contains(keywordLower)) {
|
||||
result.add(contact);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. 匹配姓名的拼音首字母(包含关键词)
|
||||
String namePinyinFirstLetter = getPinyinFirstLetter(name).toLowerCase();
|
||||
if (namePinyinFirstLetter.contains(keywordLower)) {
|
||||
result.add(contact);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将汉字转为全拼(不带声调,小写)
|
||||
* 例如:"张三" → "zhangsan"
|
||||
*/
|
||||
private String getPinyin(String chinese) {
|
||||
StringBuilder pinyin = new StringBuilder();
|
||||
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
|
||||
format.setCaseType(HanyuPinyinCaseType.LOWERCASE); // 小写
|
||||
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE); // 不带声调
|
||||
|
||||
char[] chars = chinese.toCharArray();
|
||||
for (char c : chars) {
|
||||
// 如果是汉字,转换为拼音;否则直接拼接(如字母、数字、符号)
|
||||
if (Character.toString(c).matches("[\\u4e00-\\u9fa5]")) {
|
||||
try {
|
||||
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(c, format);
|
||||
if (pinyinArray != null && pinyinArray.length > 0) {
|
||||
pinyin.append(pinyinArray[0]); // 取第一个拼音(多音字默认取第一个)
|
||||
}
|
||||
} catch (BadHanyuPinyinOutputFormatCombination e) {
|
||||
LogUtils.e(TAG, "拼音转换失败:" + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
pinyin.append(c);
|
||||
}
|
||||
}
|
||||
return pinyin.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将汉字转为拼音首字母(小写)
|
||||
* 例如:"张三" → "zs"
|
||||
*/
|
||||
private String getPinyinFirstLetter(String chinese) {
|
||||
StringBuilder firstLetters = new StringBuilder();
|
||||
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
|
||||
format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
|
||||
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
|
||||
|
||||
char[] chars = chinese.toCharArray();
|
||||
for (char c : chars) {
|
||||
if (Character.toString(c).matches("[\\u4e00-\\u9fa5]")) {
|
||||
try {
|
||||
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(c, format);
|
||||
if (pinyinArray != null && pinyinArray.length > 0) {
|
||||
// 取拼音首字母(如"zhang" → "z")
|
||||
firstLetters.append(pinyinArray[0].charAt(0));
|
||||
}
|
||||
} catch (BadHanyuPinyinOutputFormatCombination e) {
|
||||
LogUtils.e(TAG, "拼音首字母转换失败:" + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
// 非汉字直接拼接首字符(如"李3" → "l3")
|
||||
firstLetters.append(c);
|
||||
}
|
||||
}
|
||||
return firstLetters.toString();
|
||||
}
|
||||
|
||||
public boolean isPhoneInContacts(String szPhone) {
|
||||
List<PhoneBean> listPhoneDto = getPhoneList();
|
||||
LogUtils.d(TAG, String.format("isPhoneInContacts(...) listPhoneDto.size() %d", listPhoneDto.size()));
|
||||
@@ -70,49 +186,56 @@ public class PhoneUtil {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public String getNameByPhone(String szPhone) {
|
||||
if (szPhone == null || szPhone.equals("")) {
|
||||
return "";
|
||||
}
|
||||
|
||||
List<PhoneBean> listPhoneDto = getPhoneList();
|
||||
LogUtils.d(TAG, String.format("getNameByPhone(...) listPhoneDto.size() %d", listPhoneDto.size()));
|
||||
for (int i = 0; i < listPhoneDto.size(); i++) {
|
||||
if (isTheSamePhoneNumber(listPhoneDto.get(i).getTelPhone(), szPhone)) {
|
||||
return listPhoneDto.get(i).getName();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
boolean isTheSamePhoneNumber(String szNum1, String szNum2) {
|
||||
//LogUtils.d(TAG, String.format("szNum1 %s\nszNum2 %s", szNum1, szNum2));
|
||||
if(szNum1.equals(szNum2)) {
|
||||
if (szNum1.equals(szNum2)) {
|
||||
LogUtils.d(TAG, "szNum1.equals(szNum2)");
|
||||
return true;
|
||||
}
|
||||
|
||||
if(UnitAreaUtils.getInstance(mContext).isCurrentUnitAreaNumber(szNum1)) {
|
||||
if(szNum1.equals(UnitAreaUtils.getInstance(mContext).genCurrentUnitAreaNumber(szNum2))) {
|
||||
|
||||
if (UnitAreaUtils.getInstance(mContext).isCurrentUnitAreaNumber(szNum1)) {
|
||||
if (szNum1.equals(UnitAreaUtils.getInstance(mContext).genCurrentUnitAreaNumber(szNum2))) {
|
||||
LogUtils.d(TAG, "szNum1.equals(UnitAreaUtils.genCurrentUnitAreaNumber(szNum2))");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if(UnitAreaUtils.getInstance(mContext).isCurrentUnitAreaNumber(szNum2)) {
|
||||
if(szNum2.equals(UnitAreaUtils.getInstance(mContext).genCurrentUnitAreaNumber(szNum1))) {
|
||||
|
||||
if (UnitAreaUtils.getInstance(mContext).isCurrentUnitAreaNumber(szNum2)) {
|
||||
if (szNum2.equals(UnitAreaUtils.getInstance(mContext).genCurrentUnitAreaNumber(szNum1))) {
|
||||
LogUtils.d(TAG, "szNum2.equals(UnitAreaUtils.genCurrentUnitAreaNumber(szNum1))");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
LogUtils.d(TAG, "isTheSamePhoneNumber(...) return false;");
|
||||
return false;
|
||||
}
|
||||
|
||||
//
|
||||
// 检验电话号码是否是数字
|
||||
//
|
||||
public static boolean isPhoneByDigit(String szPhone) {
|
||||
if(!RegexPPiUtils.isPPiOK(szPhone)) {
|
||||
if (!RegexPPiUtils.isPPiOK(szPhone)) {
|
||||
return false;
|
||||
}
|
||||
//String text = "这里是一些任意的文本内容";
|
||||
String regex = "[+]?\\d+";
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher matcher = pattern.matcher(szPhone);
|
||||
LogUtils.d(TAG, String.format("matcher.matches() : %s", matcher.matches()));
|
||||
/*if (matcher.matches()) {
|
||||
System.out.println("文本满足该正则表达式模式");
|
||||
} else {
|
||||
System.out.println("文本不满足该正则表达式模式");
|
||||
}*/
|
||||
return matcher.matches();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -10,9 +10,9 @@ import cc.winboll.studio.mymessagemanager.beans.AppConfigBean;
|
||||
import android.content.Context;
|
||||
|
||||
public class UnitAreaUtils {
|
||||
|
||||
|
||||
public static final String TAG = "UnitAreaUtils";
|
||||
|
||||
|
||||
static UnitAreaUtils _UnitAreaUtils;
|
||||
Context mContext;
|
||||
|
||||
@@ -26,19 +26,25 @@ public class UnitAreaUtils {
|
||||
}
|
||||
return _UnitAreaUtils;
|
||||
}
|
||||
|
||||
|
||||
public boolean isCurrentUnitAreaNumber(String szPhoneNumer) {
|
||||
String szUnitArea = getUnitArea();
|
||||
LogUtils.d(TAG, String.format("szPhoneNumer.substring(1,3) %s", szPhoneNumer.substring(1,3)));
|
||||
return szPhoneNumer.substring(1,3).equals(szUnitArea);
|
||||
try {
|
||||
String szPhoneNumerUnitArea = szPhoneNumer.substring(1, 3);
|
||||
LogUtils.d(TAG, String.format("szPhoneNumerUnitArea %s", szPhoneNumerUnitArea));
|
||||
return szPhoneNumerUnitArea.equals(szUnitArea);
|
||||
} catch (StringIndexOutOfBoundsException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public String genCurrentUnitAreaNumber(String szPhoneNumer) {
|
||||
String szUnitArea = getUnitArea();
|
||||
LogUtils.d(TAG, String.format("szUnitArea %s", szUnitArea));
|
||||
return "+" + szUnitArea + szPhoneNumer;
|
||||
}
|
||||
|
||||
|
||||
String getUnitArea() {
|
||||
String szUnitArea = AppConfigUtil.getInstance(mContext).mAppConfigBean.getCountryCode();
|
||||
LogUtils.d(TAG, String.format("szUnitArea %s", szUnitArea));
|
||||
|
@@ -0,0 +1,125 @@
|
||||
package cc.winboll.studio.mymessagemanager.views;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@188.com>
|
||||
* @Date 2025/08/23 00:39
|
||||
* @Describe 多级拉动响应自定义控件
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
public class BottomPositionFixedScrollView extends ScrollView {
|
||||
public static final String TAG = "BottomPositionFixedScrollView";
|
||||
// 记录底部对应的内容绝对位置(即底部位置在内容中的y坐标,该位置需始终保持在视图底部)
|
||||
private int mBottomContentY = 0;
|
||||
// 标记是否是首次布局(避免初始加载误触发)
|
||||
private boolean isFirstLayout = true;
|
||||
|
||||
public BottomPositionFixedScrollView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public BottomPositionFixedScrollView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public BottomPositionFixedScrollView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
// 监听布局变化(高度改变时触发)
|
||||
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
if (isFirstLayout) {
|
||||
isFirstLayout = false;
|
||||
return;
|
||||
}
|
||||
// 布局变化后,恢复底部位置
|
||||
restoreBottomPosition();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写滚动事件,记录“底部对应的内容绝对位置”
|
||||
* (即当前视图底部边缘对应的内容y坐标,该坐标需始终保持在视图底部)
|
||||
*/
|
||||
@Override
|
||||
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
|
||||
super.onScrollChanged(l, t, oldl, oldt);
|
||||
if (getChildCount() == 0) {
|
||||
mBottomContentY = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// 内容总高度
|
||||
int contentHeight = getChildAt(0).getMeasuredHeight();
|
||||
// 视图可视高度(自身高度)
|
||||
int scrollViewHeight = getMeasuredHeight();
|
||||
// 当前视图底部边缘对应的内容y坐标 = 顶部滚动距离(t) + 可视高度
|
||||
// (该坐标就是“底部内容的绝对位置”,需始终保持在视图底部)
|
||||
mBottomContentY = t + scrollViewHeight;
|
||||
|
||||
// 避免超过内容总高度(比如内容不足一屏时,底部最多到内容底部)
|
||||
if (mBottomContentY > contentHeight) {
|
||||
mBottomContentY = contentHeight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复底部位置:让原记录的“底部内容绝对位置”仍保持在视图底部
|
||||
*/
|
||||
private void restoreBottomPosition() {
|
||||
if (getChildCount() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 新的内容总高度
|
||||
int newContentHeight = getChildAt(0).getMeasuredHeight();
|
||||
// 新的视图可视高度
|
||||
int newScrollViewHeight = getMeasuredHeight();
|
||||
|
||||
// 目标:让原mBottomContentY(底部内容绝对位置)仍位于视图底部
|
||||
// 此时需要的顶部滚动距离 = mBottomContentY - 新的可视高度
|
||||
int targetScrollY = mBottomContentY - newScrollViewHeight;
|
||||
|
||||
// 边界修正:
|
||||
// 1. 不能小于0(避免滚动到负数位置)
|
||||
// 2. 不能大于“最大可滚动距离”(内容高度 - 可视高度,避免超出内容范围)
|
||||
int maxScrollY = Math.max(newContentHeight - newScrollViewHeight, 0);
|
||||
targetScrollY = Math.max(targetScrollY, 0);
|
||||
targetScrollY = Math.min(targetScrollY, maxScrollY);
|
||||
|
||||
// 滚动到目标位置,保持底部内容位置不变
|
||||
smoothScrollTo(0, targetScrollY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 外部手动设置底部内容绝对位置(可选)
|
||||
*/
|
||||
public void setBottomContentY(int bottomContentY) {
|
||||
if (getChildCount() == 0) {
|
||||
mBottomContentY = bottomContentY;
|
||||
return;
|
||||
}
|
||||
// 限制不超过内容总高度
|
||||
int contentHeight = getChildAt(0).getMeasuredHeight();
|
||||
mBottomContentY = Math.min(bottomContentY, contentHeight);
|
||||
restoreBottomPosition();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前底部内容绝对位置(可选)
|
||||
*/
|
||||
public int getBottomContentY() {
|
||||
return mBottomContentY;
|
||||
}
|
||||
}
|
||||
|
@@ -21,25 +21,63 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/activitycomposesmsRelativeLayout1">
|
||||
|
||||
<TextView
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="SMS TO : "
|
||||
android:id="@+id/activitycomposesmsTextView1"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_centerVertical="true"/>
|
||||
|
||||
<EditText
|
||||
android:layout_toRightOf="@id/activitycomposesmsTextView1"
|
||||
android:layout_width="wrap_content"
|
||||
android:inputType="phone"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:id="@+id/activitycomposesmsEditText1"
|
||||
android:id="@+id/activitycomposesmsLinearLayout1"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_centerVertical="true"/>
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_alignParentLeft="true">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="(拼音搜索):"/>
|
||||
|
||||
<EditText
|
||||
android:layout_width="80dp"
|
||||
android:ems="10"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/activitycomposesmsEditText2"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_toRightOf="@id/activitycomposesmsEditText2"
|
||||
android:id="@+id/activitycomposesmsTextView2"
|
||||
android:layout_weight="1.0"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_below="@id/activitycomposesmsLinearLayout1"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="(SMS TO) :"
|
||||
android:id="@+id/activitycomposesmsTextView1"/>
|
||||
|
||||
<EditText
|
||||
android:layout_width="wrap_content"
|
||||
android:inputType="phone"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:id="@+id/activitycomposesmsEditText1"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
@@ -10,36 +10,40 @@
|
||||
android:layout_height="@dimen/toolbar_height"
|
||||
android:id="@+id/activitysmsASupportToolbar1"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:orientation="vertical"
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="10dp">
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/activitysmsScrollView2">
|
||||
|
||||
<ScrollView
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_above="@+id/activitysmsinclude1"
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:id="@+id/activitysmsinphoneScrollView1"
|
||||
android:layout_weight="1.0"
|
||||
android:isScrollContainer="false">
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/activitysmsLinearLayout1">
|
||||
|
||||
<cc.winboll.studio.mymessagemanager.views.SMSListViewForScrollView
|
||||
<cc.winboll.studio.mymessagemanager.views.BottomPositionFixedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="520dp"
|
||||
android:isScrollContainer="false"
|
||||
android:id="@+id/activitysmsScrollView1">
|
||||
|
||||
<cc.winboll.studio.mymessagemanager.views.SMSListViewForScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/activitysmsSMSListViewForScrollView1"/>
|
||||
|
||||
</cc.winboll.studio.mymessagemanager.views.BottomPositionFixedScrollView>
|
||||
|
||||
<include
|
||||
layout="@layout/view_smssend"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/activitysmsinphoneListView1"/>
|
||||
android:id="@+id/activitysmsinclude1"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<include
|
||||
android:layout_alignParentBottom="true"
|
||||
layout="@layout/view_smssend"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/activitysmsinclude1"/>
|
||||
|
||||
</RelativeLayout>
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
@@ -34,7 +34,7 @@
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<cc.winboll.studio.shared.log.LogView
|
||||
<cc.winboll.studio.libappbase.LogView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1.0"
|
||||
|
@@ -7,6 +7,18 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="10dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textSize="20sp"
|
||||
android:id="@+id/viewsmssendpart1TextView1"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AOHPCTCSeekBar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/viewsmssendpart1AOHPCTCSeekBar1"/>
|
||||
|
||||
<EditText
|
||||
android:scrollbars="vertical"
|
||||
android:maxHeight="150dp"
|
||||
@@ -17,17 +29,6 @@
|
||||
android:id="@+id/viewsmssendpart1EditText1"
|
||||
android:background="@drawable/bg_frame"/>
|
||||
|
||||
<cc.winboll.studio.libaes.views.AOHPCTCSeekBar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/viewsmssendpart1AOHPCTCSeekBar1"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textSize="20sp"
|
||||
android:id="@+id/viewsmssendpart1TextView1"/>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
@@ -17,26 +17,4 @@
|
||||
<item
|
||||
android:id="@+id/app_smsrule"
|
||||
android:title="@string/text_smsrule"/>
|
||||
<item android:title="@string/app_developoptions">
|
||||
<menu>
|
||||
<item
|
||||
android:id="@+id/app_log"
|
||||
android:title="@string/app_log"/>
|
||||
<item
|
||||
android:id="@+id/app_unittest"
|
||||
android:title="@string/app_unittest"/>
|
||||
<item
|
||||
android:id="@+id/app_crashtest"
|
||||
android:title="@string/app_crashtest"/>
|
||||
</menu>
|
||||
</item>
|
||||
<item
|
||||
android:id="@+id/app_appsettings"
|
||||
android:title="@string/text_appsettings"/>
|
||||
<item
|
||||
android:id="@+id/app_about"
|
||||
android:title="@string/app_about"/>
|
||||
<item
|
||||
android:id="@+id/app_smsrecycle"
|
||||
android:title="@string/app_smsrecycle"/>
|
||||
</menu>
|
||||
|
26
mymessagemanager/src/main/res/menu/toolbar_main2.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item android:title="@string/app_developoptions">
|
||||
<menu>
|
||||
<item
|
||||
android:id="@+id/app_log"
|
||||
android:title="@string/app_log"/>
|
||||
<item
|
||||
android:id="@+id/app_unittest"
|
||||
android:title="@string/app_unittest"/>
|
||||
<item
|
||||
android:id="@+id/app_crashtest"
|
||||
android:title="@string/app_crashtest"/>
|
||||
</menu>
|
||||
</item>
|
||||
<item
|
||||
android:id="@+id/app_appsettings"
|
||||
android:title="@string/text_appsettings"/>
|
||||
<item
|
||||
android:id="@+id/app_about"
|
||||
android:title="@string/app_about"/>
|
||||
<item
|
||||
android:id="@+id/app_smsrecycle"
|
||||
android:title="@string/app_smsrecycle"/>
|
||||
</menu>
|
@@ -41,6 +41,6 @@
|
||||
<string name="text_appsettings">应用设置</string>
|
||||
<string name="text_ttsplaydelaytimes">TTS播放延迟时间(秒):</string>
|
||||
<string name="msg_newsms">接收到新的消息。</string>
|
||||
<string name="msg_100sendmsg">>>>拉图标动到 100% 以发送信息。>>></string>
|
||||
<string name="msg_100applysettings">>>>拉图标动到 100% 应用设置。>>></string>
|
||||
<string name="msg_100sendmsg">>>>拉动到100%可发信息>>></string>
|
||||
<string name="msg_100applysettings">>>>拉动到100%可应用设置>>></string>
|
||||
</resources>
|
||||
|
1
numtable/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
34
numtable/README.md
Normal file
@@ -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<zhangsken@188.com>)
|
||||
4. 新建 Pull Request
|
||||
|
||||
|
||||
#### 特技
|
||||
|
||||
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
|
||||
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
|
||||
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
|
||||
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
|
||||
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
|
||||
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|
||||
|
||||
#### 参考文档
|
1
numtable/app_update_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
73
numtable/build.gradle
Normal file
@@ -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'
|
||||
}
|
8
numtable/build.properties
Normal file
@@ -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
|
21
numtable/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
14
numtable/src/beta/AndroidManifest.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools" >
|
||||
|
||||
<application
|
||||
tools:replace="android:icon"
|
||||
android:icon="@drawable/ic_launcher_beta">
|
||||
|
||||
<!-- Put flavor specific code here -->
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
6
numtable/src/beta/res/values/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="app_name">NumTable +</string>
|
||||
|
||||
</resources>
|
37
numtable/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="cc.winboll.studio.numtable">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:roundIcon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/MyAppTheme"
|
||||
android:resizeableActivity="true"
|
||||
android:name=".App">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="4.0"/>
|
||||
|
||||
<activity android:name=".GlobalApplication$CrashActivity"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
345
numtable/src/main/java/cc/winboll/studio/numtable/App.java
Normal file
@@ -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<String, String> head = new LinkedHashMap<String, String>();
|
||||
head.put("Time Of Crash", time);
|
||||
head.put("Device", String.format("%s, %s", Build.MANUFACTURER, Build.MODEL));
|
||||
head.put("Android Version", String.format("%s (%d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
|
||||
head.put("App Version", String.format("%s (%d)", versionName, versionCode));
|
||||
head.put("Kernel", getKernel());
|
||||
head.put("Support Abis", Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS != null ? Arrays.toString(Build.SUPPORTED_ABIS): "unknown");
|
||||
head.put("Fingerprint", Build.FINGERPRINT);
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
for (String key : head.keySet()) {
|
||||
if (builder.length() != 0) builder.append("\n");
|
||||
builder.append(key);
|
||||
builder.append(" : ");
|
||||
builder.append(head.get(key));
|
||||
}
|
||||
|
||||
builder.append("\n\n");
|
||||
builder.append(Log.getStackTraceString(throwable));
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private void writeLog(String log) {
|
||||
String time = DATE_FORMAT.format(new Date());
|
||||
File file = new File(mCrashDir, "crash_" + time + ".txt");
|
||||
try {
|
||||
write(file, log.getBytes("UTF-8"));
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private static String getKernel() {
|
||||
try {
|
||||
return App.toString(new FileInputStream("/proc/version")).trim();
|
||||
} catch (Throwable e) {
|
||||
return e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class CrashActivity extends Activity {
|
||||
|
||||
private String mLog;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setTheme(android.R.style.Theme_DeviceDefault);
|
||||
setTitle("App Crash");
|
||||
|
||||
mLog = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
||||
|
||||
ScrollView contentView = new ScrollView(this);
|
||||
contentView.setFillViewport(true);
|
||||
|
||||
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(this);
|
||||
|
||||
TextView textView = new TextView(this);
|
||||
int padding = dp2px(16);
|
||||
textView.setPadding(padding, padding, padding, padding);
|
||||
textView.setText(mLog);
|
||||
textView.setTextIsSelectable(true);
|
||||
textView.setTypeface(Typeface.DEFAULT);
|
||||
textView.setLinksClickable(true);
|
||||
|
||||
horizontalScrollView.addView(textView);
|
||||
contentView.addView(horizontalScrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
|
||||
setContentView(contentView);
|
||||
}
|
||||
|
||||
private void restart() {
|
||||
Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
|
||||
if (intent != null) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
}
|
||||
finish();
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private static int dp2px(float dpValue) {
|
||||
final float scale = Resources.getSystem().getDisplayMetrics().density;
|
||||
return (int) (dpValue * scale + 0.5f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
menu.add(0, android.R.id.copy, 0, android.R.string.copy)
|
||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.copy:
|
||||
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
restart();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="78.5885"
|
||||
android:endY="90.9159"
|
||||
android:startX="48.7653"
|
||||
android:startY="61.0927"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1" />
|
||||
</vector>
|
13
numtable/src/main/res/drawable/ic_launcher.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="512dp"
|
||||
android:height="512dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:fillColor="#FF3D8A1C"
|
||||
android:strokeColor="#FFF5DD00"
|
||||
android:strokeWidth="20.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeMiterLimit="10"
|
||||
android:pathData="M47.53 417.75C38.23 258.17 97.09 94.95 156.27 94.16 211.66 98.38 260.9 237.99 329.08 242.47 351.54 241.69 397.94 228.17 449.46 88.59 447.52 262.11 394.97 417.45 338.71 418.74 279.06 412.93 220.17 297.22 157.81 295.26 132.98 294.7 116.37 308.69 47.53 417.75Z"/>
|
||||
</vector>
|
170
numtable/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillColor="#FFFBC41E"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
</vector>
|
11
numtable/src/main/res/drawable/ic_launcher_beta.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:clickable="true">
|
||||
<item android:drawable="@drawable/ic_launcher_background"/>
|
||||
<item
|
||||
android:left="15dp"
|
||||
android:top="15dp"
|
||||
android:right="15dp"
|
||||
android:bottom="15dp"
|
||||
android:drawable="@drawable/ic_launcher"/>
|
||||
</layer-list>
|
51
numtable/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,51 @@
|
||||
<?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:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1.0"
|
||||
android:gravity="center_vertical|center_horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="NumTable"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"/>
|
||||
|
||||
</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>
|
||||
|
5
numtable/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
BIN
numtable/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
numtable/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
numtable/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
numtable/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
numtable/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
numtable/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
numtable/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
numtable/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
numtable/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
numtable/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 15 KiB |
6
numtable/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#009688</color>
|
||||
<color name="colorPrimaryDark">#00796B</color>
|
||||
<color name="colorAccent">#FF9800</color>
|
||||
</resources>
|
4
numtable/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<resources>
|
||||
<string name="app_name">NumTable</string>
|
||||
|
||||
</resources>
|
11
numtable/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="MyAppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
12
numtable/src/stage/AndroidManifest.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools" >
|
||||
|
||||
<application>
|
||||
|
||||
<!-- Put flavor specific code here -->
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
6
numtable/src/stage/res/values/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- Put flavor specific strings here -->
|
||||
|
||||
</resources>
|
1
ollama/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
1
ollama/app_update_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
73
ollama/build.gradle
Normal file
@@ -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.ollama"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 29
|
||||
versionCode 1
|
||||
// versionName 更新后需要手动设置
|
||||
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
|
||||
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||
versionName "15.0"
|
||||
if(true) {
|
||||
versionName = genVersionName("${versionName}")
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
api 'com.squareup.okhttp3:okhttp:4.4.1'
|
||||
|
||||
// 吐司类库
|
||||
api 'com.github.getActivity:ToastUtils:10.5'
|
||||
|
||||
// Android 类库
|
||||
api 'com.android.support:appcompat-v7:28.0.0' // 包含 AppCompatActivity
|
||||
// https://mvnrepository.com/artifact/com.android.support/support-compat
|
||||
api 'com.android.support:support-compat:28.0.0' // 保留原有依赖(可选)
|
||||
// https://mvnrepository.com/artifact/com.android.support/support-v4
|
||||
api 'com.android.support:support-v4:28.0.0'
|
||||
// https://mvnrepository.com/artifact/com.android.support/support-media-compat
|
||||
api 'com.android.support:support-media-compat:28.0.0'
|
||||
// https://mvnrepository.com/artifact/com.android.support/support-core-utils
|
||||
api 'com.android.support:support-core-utils:28.0.0'
|
||||
// https://mvnrepository.com/artifact/com.android.support/support-core-ui
|
||||
api 'com.android.support:support-core-ui:28.0.0'
|
||||
// https://mvnrepository.com/artifact/com.android.support/support-fragment
|
||||
api 'com.android.support:support-fragment:28.0.0'
|
||||
// https://mvnrepository.com/artifact/com.android.support/recyclerview-v7
|
||||
api 'com.android.support:recyclerview-v7:28.0.0'
|
||||
|
||||
api 'cc.winboll.studio:libappbase:15.0.9'
|
||||
api 'cc.winboll.studio:libapputils:15.0.11'
|
||||
}
|
8
ollama/build.properties
Normal file
@@ -0,0 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Fri Mar 28 05:09:21 HKT 2025
|
||||
stageCount=1
|
||||
libraryProject=
|
||||
baseVersion=15.0
|
||||
publishVersion=15.0.0
|
||||
buildCount=0
|
||||
baseBetaVersion=15.0.1
|
21
ollama/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
12
ollama/src/beta/AndroidManifest.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools" >
|
||||
|
||||
<application>
|
||||
|
||||
<!-- Put flavor specific code here -->
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
7
ollama/src/beta/res/values/strings.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
|
||||
<string name="app_name">Ollama +</string>
|
||||
|
||||
</resources>
|
41
ollama/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="cc.winboll.studio.ollama">
|
||||
|
||||
<!-- 拥有完全的网络访问权限 -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<application
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/MyAppTheme"
|
||||
android:resizeableActivity="true"
|
||||
android:name=".App"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="4.0"/>
|
||||
|
||||
<activity android:name=".GlobalApplication$CrashActivity"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
334
ollama/src/main/java/cc/winboll/studio/ollama/App.java
Normal file
@@ -0,0 +1,334 @@
|
||||
package cc.winboll.studio.ollama;
|
||||
|
||||
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.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 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();
|
||||
//CrashHandler.getInstance().registerGlobal(this);
|
||||
//CrashHandler.getInstance().registerPart(this);
|
||||
}
|
||||
|
||||
public static void write(InputStream input, OutputStream output) throws IOException {
|
||||
byte[] buf = new byte[1024 * 8];
|
||||
int len;
|
||||
while ((len = input.read(buf)) != -1) {
|
||||
output.write(buf, 0, len);
|
||||
}
|
||||
}
|
||||
|
||||
public static void write(File file, byte[] data) throws IOException {
|
||||
File parent = file.getParentFile();
|
||||
if (parent != null && !parent.exists()) parent.mkdirs();
|
||||
|
||||
ByteArrayInputStream input = new ByteArrayInputStream(data);
|
||||
FileOutputStream output = new FileOutputStream(file);
|
||||
try {
|
||||
write(input, output);
|
||||
} finally {
|
||||
closeIO(input, output);
|
||||
}
|
||||
}
|
||||
|
||||
public static String toString(InputStream input) throws IOException {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
write(input, output);
|
||||
try {
|
||||
return output.toString("UTF-8");
|
||||
} finally {
|
||||
closeIO(input, output);
|
||||
}
|
||||
}
|
||||
|
||||
public static void closeIO(Closeable... closeables) {
|
||||
for (Closeable closeable : closeables) {
|
||||
try {
|
||||
if (closeable != null) closeable.close();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public static class CrashHandler {
|
||||
|
||||
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
|
||||
|
||||
private static CrashHandler sInstance;
|
||||
|
||||
private PartCrashHandler mPartCrashHandler;
|
||||
|
||||
public static CrashHandler getInstance() {
|
||||
if (sInstance == null) {
|
||||
sInstance = new CrashHandler();
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public void registerGlobal(Context context) {
|
||||
registerGlobal(context, null);
|
||||
}
|
||||
|
||||
public void registerGlobal(Context context, String crashDir) {
|
||||
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir));
|
||||
}
|
||||
|
||||
public void unregister() {
|
||||
Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER);
|
||||
}
|
||||
|
||||
public void registerPart(Context context) {
|
||||
unregisterPart(context);
|
||||
mPartCrashHandler = new PartCrashHandler(context.getApplicationContext());
|
||||
MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler);
|
||||
}
|
||||
|
||||
public void unregisterPart(Context context) {
|
||||
if (mPartCrashHandler != null) {
|
||||
mPartCrashHandler.isRunning.set(false);
|
||||
mPartCrashHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class PartCrashHandler implements Runnable {
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
public AtomicBoolean isRunning = new AtomicBoolean(true);
|
||||
|
||||
public PartCrashHandler(Context context) {
|
||||
this.mContext = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (isRunning.get()) {
|
||||
try {
|
||||
Looper.loop();
|
||||
} catch (final Throwable e) {
|
||||
e.printStackTrace();
|
||||
if (isRunning.get()) {
|
||||
MAIN_HANDLER.post(new Runnable(){
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (e instanceof RuntimeException) {
|
||||
throw (RuntimeException)e;
|
||||
} else {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler {
|
||||
|
||||
private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss");
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
private final File mCrashDir;
|
||||
|
||||
public UncaughtExceptionHandlerImpl(Context context, String crashDir) {
|
||||
this.mContext = context;
|
||||
this.mCrashDir = TextUtils.isEmpty(crashDir) ? new File(mContext.getExternalCacheDir(), "crash") : new File(crashDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uncaughtException(Thread thread, Throwable throwable) {
|
||||
try {
|
||||
|
||||
String log = buildLog(throwable);
|
||||
writeLog(log);
|
||||
|
||||
try {
|
||||
Intent intent = new Intent(mContext, CrashActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, log);
|
||||
mContext.startActivity(intent);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
writeLog(e.toString());
|
||||
}
|
||||
|
||||
throwable.printStackTrace();
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
System.exit(0);
|
||||
|
||||
} catch (Throwable e) {
|
||||
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
|
||||
}
|
||||
}
|
||||
|
||||
private String buildLog(Throwable throwable) {
|
||||
String time = DATE_FORMAT.format(new Date());
|
||||
|
||||
String versionName = "unknown";
|
||||
long versionCode = 0;
|
||||
try {
|
||||
PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
|
||||
versionName = packageInfo.versionName;
|
||||
versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
|
||||
} catch (Throwable ignored) {}
|
||||
|
||||
LinkedHashMap<String, String> head = new LinkedHashMap<String, String>();
|
||||
head.put("Time Of Crash", time);
|
||||
head.put("Device", String.format("%s, %s", Build.MANUFACTURER, Build.MODEL));
|
||||
head.put("Android Version", String.format("%s (%d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
|
||||
head.put("App Version", String.format("%s (%d)", versionName, versionCode));
|
||||
head.put("Kernel", getKernel());
|
||||
head.put("Support Abis", Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS != null ? Arrays.toString(Build.SUPPORTED_ABIS): "unknown");
|
||||
head.put("Fingerprint", Build.FINGERPRINT);
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
for (String key : head.keySet()) {
|
||||
if (builder.length() != 0) builder.append("\n");
|
||||
builder.append(key);
|
||||
builder.append(" : ");
|
||||
builder.append(head.get(key));
|
||||
}
|
||||
|
||||
builder.append("\n\n");
|
||||
builder.append(Log.getStackTraceString(throwable));
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private void writeLog(String log) {
|
||||
String time = DATE_FORMAT.format(new Date());
|
||||
File file = new File(mCrashDir, "crash_" + time + ".txt");
|
||||
try {
|
||||
write(file, log.getBytes("UTF-8"));
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private static String getKernel() {
|
||||
try {
|
||||
return App.toString(new FileInputStream("/proc/version")).trim();
|
||||
} catch (Throwable e) {
|
||||
return e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class CrashActivity extends Activity {
|
||||
|
||||
private String mLog;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setTheme(android.R.style.Theme_DeviceDefault);
|
||||
setTitle("App Crash");
|
||||
|
||||
mLog = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
||||
|
||||
ScrollView contentView = new ScrollView(this);
|
||||
contentView.setFillViewport(true);
|
||||
|
||||
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(this);
|
||||
|
||||
TextView textView = new TextView(this);
|
||||
int padding = dp2px(16);
|
||||
textView.setPadding(padding, padding, padding, padding);
|
||||
textView.setText(mLog);
|
||||
textView.setTextIsSelectable(true);
|
||||
textView.setTypeface(Typeface.DEFAULT);
|
||||
textView.setLinksClickable(true);
|
||||
|
||||
horizontalScrollView.addView(textView);
|
||||
contentView.addView(horizontalScrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
|
||||
setContentView(contentView);
|
||||
}
|
||||
|
||||
private void restart() {
|
||||
Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
|
||||
if (intent != null) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
}
|
||||
finish();
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private static int dp2px(float dpValue) {
|
||||
final float scale = Resources.getSystem().getDisplayMetrics().density;
|
||||
return (int) (dpValue * scale + 0.5f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
menu.add(0, android.R.id.copy, 0, android.R.string.copy)
|
||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.copy:
|
||||
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
restart();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,88 @@
|
||||
package cc.winboll.studio.ollama;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
public final static int MSG_APPEND = 0;
|
||||
|
||||
private Handler _Handler = new Handler(Looper.getMainLooper());
|
||||
private TextView mtvMessage;
|
||||
private EditText metAsk;
|
||||
private Button mbtSend;
|
||||
private ScrollView msvMessage;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
mtvMessage = (TextView) findViewById(R.id.message_tv);
|
||||
metAsk = (EditText) findViewById(R.id.ask_et);
|
||||
mbtSend = (Button) findViewById(R.id.send_bt);
|
||||
msvMessage = findViewById(R.id.message_sv);
|
||||
|
||||
mbtSend.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
sendQuestion();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// 设置输入框获得焦点的类
|
||||
//
|
||||
// static class MyHandler extends Handler {
|
||||
// WeakReference<MainActivity> mActivity;
|
||||
// MyHandler(MainActivity activity) {
|
||||
// mActivity = new WeakReference<MainActivity>(activity);
|
||||
// }
|
||||
// public void handleMessage(Message msg) {
|
||||
// MainActivity theActivity = mActivity.get();
|
||||
// switch (msg.what) {
|
||||
// case MSG_APPEND:
|
||||
// theActivity.mtvMessage.append((String)msg.obj);
|
||||
// break;
|
||||
// default:
|
||||
// break;
|
||||
// }
|
||||
// super.handleMessage(msg);
|
||||
// }
|
||||
// }
|
||||
|
||||
private void sendQuestion() {
|
||||
final String question = metAsk.getText().toString().trim();
|
||||
if (!question.equals("")) {
|
||||
mtvMessage.append("\n\nI :" + metAsk.getText().toString() + "\nOllama : ");
|
||||
metAsk.setText("");
|
||||
new OllamaClient.SyncAskThread(question, new OllamaClient.OnAnswerCallback() {
|
||||
@Override
|
||||
public void onAnswer(final String answer) {
|
||||
_Handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mtvMessage.append(answer);
|
||||
msvMessage.post(new Runnable(){
|
||||
@Override
|
||||
public void run() {
|
||||
msvMessage.fullScroll(View.FOCUS_DOWN);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
223
ollama/src/main/java/cc/winboll/studio/ollama/OllamaClient.java
Normal file
@@ -0,0 +1,223 @@
|
||||
package cc.winboll.studio.ollama;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen@AliYun.Com
|
||||
* @Date 2025/03/27 19:55:28
|
||||
* @Describe 简单Http协议访问客户端
|
||||
*/
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.IOException;
|
||||
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 okhttp3.ResponseBody;
|
||||
import okio.Buffer;
|
||||
import okio.Source;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class OllamaClient {
|
||||
|
||||
public static final String TAG = "OllamaClient";
|
||||
|
||||
private static final String API_BASE_URL = "https://ollama-api.winboll.cc";
|
||||
//private static final String API_BASE_URL = "http://10.8.0.10:11434";
|
||||
//private static final OkHttpClient client = new OkHttpClient();
|
||||
private static final OkHttpClient client = new OkHttpClient.Builder()
|
||||
.connectTimeout(60, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.writeTimeout(60, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
// 1. 生成文本示例
|
||||
// static void generateText(String prompt, String model) {
|
||||
// String url = API_BASE_URL + "/api/generate";
|
||||
// try {
|
||||
// JSONObject payload = new JSONObject()
|
||||
// .put("model", model)
|
||||
// .put("prompt", prompt)
|
||||
// .put("temperature", 0.7)
|
||||
// .put("max_tokens", 200);
|
||||
//
|
||||
// Request request = new Request.Builder()
|
||||
// .url(url)
|
||||
// .post(RequestBody.create(payload.toString(), MediaType.get("application/json")))
|
||||
// .build();
|
||||
//
|
||||
// Response response = client.newCall(request).execute();
|
||||
// if (response.isSuccessful()) {
|
||||
// String result = response.body().string();
|
||||
// String formattedStream = OllamaResponseFormatter.formatStreamingResponse(result);
|
||||
//
|
||||
// // 输出示例:
|
||||
//// [2025-03-27T19:34:29.274955439Z] [llama3.1:8b] It looks like you might have miss
|
||||
//// [2025-03-27T19:34:30.482553089Z] [llama3.1:8b] pelled the word "Ollama" or perhaps said something that is not a standard word in the English language. However, I'm here to provide information and assistance on various topics, so please let me know what you meant by "Ollama." Was it related to a name, place, movie, game, or something else?
|
||||
//
|
||||
// LogUtils.d(TAG, formattedStream);
|
||||
//// JSONObject json = new JSONObject(result);
|
||||
//// LogUtils.d(TAG, "生成结果: " + json.getString("response"));
|
||||
// //System.out.println("生成结果: " + json.getString("response"));
|
||||
// } else {
|
||||
// LogUtils.d(TAG, "请求失败: " + response.code());
|
||||
// //System.out.println("请求失败: " + response.code() + " " + response.message());
|
||||
// }
|
||||
// } catch (JSONException|IOException e) {
|
||||
// LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
// }
|
||||
// }
|
||||
|
||||
// 实时输出流式响应的函数
|
||||
static void generateTextStream(String prompt, String model, final OnAnswerCallback callback) {
|
||||
String url = API_BASE_URL + "/api/generate";
|
||||
try {
|
||||
JSONObject payload = new JSONObject()
|
||||
.put("model", model)
|
||||
.put("prompt", prompt)
|
||||
.put("temperature", 0.7)
|
||||
.put("max_tokens", 200)
|
||||
.put("stream", true); // 启用流式响应
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.post(RequestBody.create(payload.toString(), MediaType.get("application/json")))
|
||||
.build();
|
||||
|
||||
LogUtils.d(TAG, "Request request");
|
||||
client.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
LogUtils.d(TAG, "请求失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
if (!response.isSuccessful()) {
|
||||
LogUtils.d(TAG, "请求失败: " + response.code());
|
||||
return;
|
||||
}
|
||||
|
||||
try (ResponseBody body = response.body()) {
|
||||
if (body == null) return;
|
||||
|
||||
// 实时流式解析
|
||||
LogUtils.d(TAG, "实时流式解析");
|
||||
Source source = body.source();
|
||||
Buffer buffer = new Buffer();
|
||||
StringBuilder fullResponse = new StringBuilder();
|
||||
boolean isDone = false;
|
||||
|
||||
while (!isDone && source.read(buffer, 1024) != -1) {
|
||||
//LogUtils.d(TAG, "!isDone");
|
||||
String chunk = buffer.readUtf8();
|
||||
String[] lines = chunk.split("\n");
|
||||
|
||||
for (String line : lines) {
|
||||
LogUtils.d(TAG, line);
|
||||
if (line.trim().startsWith("{\"model\":")) {
|
||||
LogUtils.d(TAG, line);
|
||||
String jsonStr = line;
|
||||
if (jsonStr.equals("[DONE]")) {
|
||||
isDone = true;
|
||||
LogUtils.d(TAG, "流式生成完成");
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
//LogUtils.d(TAG, jsonStr);
|
||||
JSONObject json = new JSONObject(jsonStr);
|
||||
//LogUtils.d(TAG, json.toString());
|
||||
String responseText = json.getString("response");
|
||||
//LogUtils.d(TAG, responseText);
|
||||
fullResponse.append(responseText);
|
||||
|
||||
// 实时输出
|
||||
callback.onAnswer(responseText);
|
||||
LogUtils.d(TAG, "实时响应: " + responseText);
|
||||
|
||||
// 处理完成状态
|
||||
if (json.getBoolean("done")) {
|
||||
isDone = true;
|
||||
String doneReason = json.optString("done_reason", "unknown");
|
||||
LogUtils.d(TAG, "生成完成 (原因: " + doneReason + ")");
|
||||
LogUtils.d(TAG, "完整回答: " + fullResponse.toString());
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
LogUtils.d(TAG, "JSON解析错误: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (JSONException e) {
|
||||
LogUtils.d(TAG, "JSON格式错误: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 2. 获取模型列表示例
|
||||
static void getModelList() {
|
||||
String url = API_BASE_URL + "/v1/models";
|
||||
LogUtils.d(TAG, "url : " + url);
|
||||
Request request = new Request.Builder().url(url).build();
|
||||
|
||||
try {
|
||||
Response response = client.newCall(request).execute();
|
||||
if (response.isSuccessful()) {
|
||||
LogUtils.d(TAG, response.body().string());
|
||||
// JSONArray models = new JSONArray(response.body().string());
|
||||
// //System.out.println("可用模型列表:");
|
||||
// LogUtils.d(TAG, "可用模型列表:");
|
||||
// for (int i = 0; i < models.length(); i++) {
|
||||
// JSONObject model = models.getJSONObject(i);
|
||||
// LogUtils.d(TAG, "- " + model.getString("name") + " (" + model.getString("size") + ")");
|
||||
// //System.out.println("- " + model.getString("name") + " (" + model.getString("size") + ")");
|
||||
// }
|
||||
} else {
|
||||
LogUtils.d(TAG, "获取模型列表失败: " + response.code());
|
||||
//System.out.println("获取模型列表失败: " + response.code());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
}
|
||||
|
||||
static void unittest(String ask, OnAnswerCallback callback) {
|
||||
// 获取模型列表
|
||||
getModelList();
|
||||
|
||||
// 生成文本
|
||||
generateTextStream(ask, "llama3.1:8b", callback);
|
||||
}
|
||||
|
||||
public static class SyncAskThread extends Thread {
|
||||
private String ask;
|
||||
private OnAnswerCallback callback;
|
||||
|
||||
public SyncAskThread(String ask, OnAnswerCallback callback) {
|
||||
this.ask = ask;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
super.run();
|
||||
LogUtils.d(TAG, "run() start.");
|
||||
unittest(ask, callback);
|
||||
LogUtils.d(TAG, "run() end.");
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnAnswerCallback {
|
||||
void onAnswer(String answer);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,72 @@
|
||||
package cc.winboll.studio.ollama;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen@AliYun.Com
|
||||
* @Date 2025/03/28 03:38:50
|
||||
*/
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class OllamaResponseFormatter {
|
||||
public static final String TAG = "OllamaResponseFormatter";
|
||||
|
||||
// 处理模型列表响应
|
||||
public static String formatModelList(String jsonResponse) {
|
||||
try {
|
||||
JSONObject json = new JSONObject(jsonResponse);
|
||||
JSONArray models = json.getJSONArray("data");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("可用模型列表:\n");
|
||||
|
||||
for (int i = 0; i < models.length(); i++) {
|
||||
JSONObject model = models.getJSONObject(i);
|
||||
String modelId = model.getString("id");
|
||||
sb.append(String.format("-%d. %s\n", i + 1, modelId));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
return "格式解析错误: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// 处理流式生成文本响应
|
||||
public static String formatStreamingResponse(String jsonResponse) {
|
||||
try {
|
||||
JSONObject json = new JSONObject(jsonResponse);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String responseText = json.getString("response");
|
||||
boolean isDone = json.getBoolean("done");
|
||||
|
||||
// 添加时间戳和模型标识
|
||||
String timestamp = json.getString("created_at");
|
||||
String modelName = json.getString("model");
|
||||
sb.append(String.format("[%s] [%s] ", timestamp, modelName));
|
||||
|
||||
// 处理响应内容
|
||||
if (responseText.isEmpty() && isDone) {
|
||||
sb.append("生成完成\n");
|
||||
} else {
|
||||
sb.append(responseText);
|
||||
if (isDone) {
|
||||
String doneReason = json.optString("done_reason", "unknown");
|
||||
sb.append(String.format(" (完成原因: %s)\n", doneReason));
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
return "格式解析错误: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
public static void main(String[] args) {
|
||||
// 模型列表测试
|
||||
String modelListJson = "{\"object\":\"list\",\"data\":[{...}]}";
|
||||
System.out.println(formatModelList(modelListJson));
|
||||
|
||||
// 流式响应测试
|
||||
String streamingJson = "{\"model\":\"llama3.1:8b\",\"created_at\":\"2025-03-27T19:34:29.274955439Z\",\"response\":\"It\",\"done\":false}";
|
||||
System.out.println(formatStreamingResponse(streamingJson));
|
||||
}
|
||||
}
|
||||
|
BIN
ollama/src/main/res/drawable/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
69
ollama/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,69 @@
|
||||
<?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:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:gravity="center_vertical|center_horizontal"
|
||||
android:layout_weight="1.0"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/message_sv">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:text="Asking Ollama ..."
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:id="@+id/message_tv"/>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<EditText
|
||||
android:layout_width="0dp"
|
||||
android:ems="10"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/ask_et"
|
||||
android:layout_weight="1.0"
|
||||
android:text="Hello, World!"/>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Send"
|
||||
android:id="@+id/send_bt"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
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="wrap_content"
|
||||
android:text="Text"
|
||||
android:id="@+id/logview"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
9
ollama/src/main/res/values-v21/styles.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="AppTheme" parent="@android:style/Theme.Material.Light.DarkActionBar">
|
||||
<item name="android:colorPrimary">@color/colorPrimary</item>
|
||||
<item name="android:colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="android:colorAccent">@color/colorAccent</item>
|
||||
<item name="android:navigationBarColor">?android:colorPrimary</item>
|
||||
</style>
|
||||
</resources>
|