Compare commits
40 Commits
348edc8aaf
...
9768103741
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9768103741 | ||
|
|
787b8f0d77 | ||
|
|
1d58126fd8 | ||
|
|
7b5a3d2d71 | ||
|
|
a988a9d4f6 | ||
|
|
04a67e666b | ||
|
|
a40dbcfb61 | ||
|
|
4d344b299b | ||
|
|
37b0867d34 | ||
|
|
cdfbb082d2 | ||
|
|
7e476894a7 | ||
|
|
0e8ae2e020 | ||
|
|
48623a2805 | ||
|
|
b505156211 | ||
|
|
91b30fb576 | ||
|
|
ab3ac72d54 | ||
|
|
73285c8779 | ||
|
|
fa338ec8c7 | ||
|
|
7a3a1f4bcd | ||
|
|
0e3b9dc760 | ||
|
|
f53b222b7f | ||
|
|
0c0cde8406 | ||
|
|
46967065c0 | ||
|
|
8edbff5ac1 | ||
|
|
434f8a8549 | ||
|
|
c04be60b13 | ||
|
|
641098f8fb | ||
|
|
dba54ac4b2 | ||
|
|
c6cd779889 | ||
|
|
dfb1692a04 | ||
|
|
c83c8f66b3 | ||
|
|
cd7b5f38bf | ||
|
|
0c2e73b82e | ||
|
|
7b1838ff8e | ||
|
|
73ff3d1726 | ||
|
|
a69572e216 | ||
|
|
fa79c3f807 | ||
|
|
fde4b275f7 | ||
|
|
d66d9373ff | ||
|
|
f32ed94e4e |
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sun Aug 31 23:39:16 HKT 2025
|
||||
stageCount=6
|
||||
#Mon Sep 01 07:56:33 HKT 2025
|
||||
stageCount=7
|
||||
libraryProject=libapputils
|
||||
baseVersion=15.8
|
||||
publishVersion=15.8.5
|
||||
publishVersion=15.8.6
|
||||
buildCount=0
|
||||
baseBetaVersion=15.8.6
|
||||
baseBetaVersion=15.8.7
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sun Aug 31 04:53:04 CST 2025
|
||||
stageCount=6
|
||||
#Mon Sep 01 07:56:11 HKT 2025
|
||||
stageCount=7
|
||||
libraryProject=libapputils
|
||||
baseVersion=15.8
|
||||
publishVersion=15.8.5
|
||||
publishVersion=15.8.6
|
||||
buildCount=0
|
||||
baseBetaVersion=15.8.6
|
||||
baseBetaVersion=15.8.7
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package cc.winboll.studio.libapputils.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@188.com>
|
||||
* @Date 2025/09/01 07:49
|
||||
* @Describe .* 前置预防针
|
||||
regex pointer preventive injection
|
||||
简称 RegexPPi
|
||||
*/
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class RegexPPiUtils {
|
||||
|
||||
public static final String TAG = "RegexPPiUtils";
|
||||
|
||||
//
|
||||
// 检验文本是否满足适合正则表达式模式计算
|
||||
//
|
||||
public static boolean isPPiOK(String text) {
|
||||
//String text = "这里是一些任意的文本内容";
|
||||
String regex = ".*";
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher matcher = pattern.matcher(text);
|
||||
/*if (matcher.matches()) {
|
||||
System.out.println("文本满足该正则表达式模式");
|
||||
} else {
|
||||
System.out.println("文本不满足该正则表达式模式");
|
||||
}*/
|
||||
return matcher.matches();
|
||||
}
|
||||
}
|
||||
1
midiplayer/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
34
midiplayer/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Midi Player
|
||||
|
||||
#### 介绍
|
||||
Midi 音乐播放器
|
||||
|
||||
#### 软件架构
|
||||
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
|
||||
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
|
||||
|
||||
|
||||
#### Gradle 编译说明
|
||||
调试版编译命令 :gradle assembleBetaDebug
|
||||
阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh miniplayer
|
||||
|
||||
#### 使用说明
|
||||
|
||||
#### 参与贡献
|
||||
|
||||
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/)
|
||||
|
||||
#### 参考文档
|
||||
0
midiplayer/app_update_description.txt
Normal file
73
midiplayer/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.midiplayer"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 30
|
||||
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'])
|
||||
|
||||
// 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.9.2'
|
||||
api 'cc.winboll.studio:libapputils:15.8.4'
|
||||
api 'cc.winboll.studio:libappbase:15.8.4'
|
||||
}
|
||||
8
midiplayer/build.properties
Normal file
@@ -0,0 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Tue Sep 02 12:52:51 GMT 2025
|
||||
stageCount=1
|
||||
libraryProject=
|
||||
baseVersion=15.0
|
||||
publishVersion=15.0.0
|
||||
buildCount=2
|
||||
baseBetaVersion=15.0.1
|
||||
21
midiplayer/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
midiplayer/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>
|
||||
|
||||
6
midiplayer/src/beta/res/values/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="app_name">MidiPlayer +</string>
|
||||
|
||||
</resources>
|
||||
50
midiplayer/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="cc.winboll.studio.midiplayer">
|
||||
|
||||
<!-- 读取您共享存储空间中的内容 -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- 修改或删除您共享存储空间中的内容 -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- 拥有完全的网络访问权限 -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/MyAppTheme"
|
||||
android:resizeableActivity="true"
|
||||
android:name=".App">
|
||||
|
||||
<activity
|
||||
android:name=".MidiPlayerActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<!-- 支持打开Midi文件 -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="audio/midi" />
|
||||
<data android:scheme="file" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="4.0"/>
|
||||
|
||||
<activity android:name=".GlobalApplication$CrashActivity"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
midiplayer/src/main/assets/midi/SuperMarioBrothers.mid
Normal file
BIN
midiplayer/src/main/assets/midi/Twinkle Twinkle Little Star.mid
Normal file
345
midiplayer/src/main/java/cc/winboll/studio/midiplayer/App.java
Normal file
@@ -0,0 +1,345 @@
|
||||
package cc.winboll.studio.midiplayer;
|
||||
|
||||
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,137 @@
|
||||
package cc.winboll.studio.midiplayer;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@188.com>
|
||||
* @Date 2025/06/29 10:56
|
||||
* @Describe 用于将assets/midi目录下的文件拷贝到内部存储
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.util.Log;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class AssetMidiCopier {
|
||||
public static final String TAG = "AssetMidiCopier";
|
||||
|
||||
private Context mContext;
|
||||
|
||||
public AssetMidiCopier(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拷贝assets/midi目录下所有文件到内部存储的midi文件夹
|
||||
* @return 拷贝是否成功
|
||||
*/
|
||||
public boolean copyMidiFiles() {
|
||||
AssetManager assetManager = mContext.getAssets();
|
||||
String[] midiFiles;
|
||||
|
||||
try {
|
||||
// 获取assets/midi目录下的所有文件
|
||||
midiFiles = assetManager.list("midi");
|
||||
if (midiFiles == null || midiFiles.length == 0) {
|
||||
Log.d(TAG, "assets/midi目录下没有文件");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取内部存储的目标目录(/data/data/包名/files/midi)
|
||||
File targetDir = new File(mContext.getFilesDir(), "midi");
|
||||
if (!targetDir.exists()) {
|
||||
// 创建目录(包括父目录)
|
||||
if (!targetDir.mkdirs()) {
|
||||
Log.e(TAG, "创建目标目录失败: " + targetDir.getAbsolutePath());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个拷贝文件
|
||||
for (String fileName : midiFiles) {
|
||||
// 跳过目录,只处理文件
|
||||
if (fileName.contains("/")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 源文件路径(assets/midi/文件名)
|
||||
String sourcePath = "midi/" + fileName;
|
||||
// 目标文件路径
|
||||
File targetFile = new File(targetDir, fileName);
|
||||
|
||||
// 如果文件已存在,跳过拷贝
|
||||
if (targetFile.exists()) {
|
||||
Log.d(TAG, "文件已存在,跳过: " + fileName);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 执行拷贝
|
||||
if (!copySingleFile(assetManager, sourcePath, targetFile)) {
|
||||
Log.e(TAG, "拷贝文件失败: " + fileName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "所有MIDI文件拷贝完成,共" + midiFiles.length + "个文件");
|
||||
return true;
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "拷贝过程发生错误: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拷贝单个assets文件到目标路径
|
||||
*/
|
||||
private boolean copySingleFile(AssetManager assetManager, String sourcePath, File targetFile) {
|
||||
InputStream in = null;
|
||||
FileOutputStream out = null;
|
||||
|
||||
try {
|
||||
// 打开assets中的源文件
|
||||
in = assetManager.open(sourcePath);
|
||||
// 创建目标文件输出流
|
||||
out = new FileOutputStream(targetFile);
|
||||
|
||||
// 缓冲区
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
// 读写文件
|
||||
while ((length = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, length);
|
||||
}
|
||||
|
||||
// 刷新输出流,确保数据写入
|
||||
out.flush();
|
||||
return true;
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "拷贝单个文件错误: " + e.getMessage());
|
||||
return false;
|
||||
|
||||
} finally {
|
||||
// 关闭流
|
||||
try {
|
||||
if (in != null) {
|
||||
in.close();
|
||||
}
|
||||
if (out != null) {
|
||||
out.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内部存储中MIDI文件的目录
|
||||
*/
|
||||
public File getMidiTargetDir() {
|
||||
return new File(mContext.getFilesDir(), "midi");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package cc.winboll.studio.midiplayer;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.LogView;
|
||||
import com.hjq.toast.ToastUtils;
|
||||
import java.io.File;
|
||||
|
||||
public class MainActivity extends WinBoLLActivity {
|
||||
|
||||
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_main);
|
||||
|
||||
Toolbar toolbar=(Toolbar)findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
mLogView = findViewById(R.id.logview);
|
||||
|
||||
ToastUtils.show("onCreate");
|
||||
copyAssetsMidiFiles();
|
||||
}
|
||||
|
||||
public void onOpenMidiPlayer(View view) {
|
||||
App.getWinBoLLActivityManager().startWinBoLLActivity(this, MidiPlayerActivity.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
mLogView.start();
|
||||
}
|
||||
|
||||
// 在需要拷贝的地方调用(如Activity的onCreate中)
|
||||
private void copyAssetsMidiFiles() {
|
||||
// 新建拷贝工具类实例
|
||||
final AssetMidiCopier copier = new AssetMidiCopier(this);
|
||||
|
||||
// 开启子线程执行拷贝(避免主线程阻塞)
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final boolean success = copier.copyMidiFiles();
|
||||
// 拷贝结果可通过Handler通知主线程更新UI
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (success) {
|
||||
// 拷贝成功,获取目标目录
|
||||
File midiDir = copier.getMidiTargetDir();
|
||||
LogUtils.d(TAG, "文件保存路径: " + midiDir.getAbsolutePath());
|
||||
// 可在这里加载MIDI文件
|
||||
} else {
|
||||
// 拷贝失败,提示用户
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
package cc.winboll.studio.midiplayer;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@188.com>
|
||||
* @Date 2025/06/29 10:26
|
||||
* @Describe MIDI文件解析器,用于解析MIDI文件并提取轨道信息
|
||||
*/
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MidiParser {
|
||||
public static final String TAG = "MidiParser";
|
||||
private static final Charset US_ASCII = Charset.forName("US-ASCII");
|
||||
|
||||
private InputStream mInputStream;
|
||||
private int mTrackCount; // 轨道数量
|
||||
private int mTicksPerBeat; // 每拍的ticks数(从文件头解析)
|
||||
|
||||
public MidiParser(File file) throws IOException {
|
||||
this.mInputStream = new FileInputStream(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析MIDI文件,返回包含轨道和每拍ticks数的结果(支持速度控制)
|
||||
*/
|
||||
public MidiPlayer.MidiParseResult parseWithTicks() throws IOException {
|
||||
try {
|
||||
// 1. 验证MIDI文件头(MThd)
|
||||
if (!verifyHeader()) {
|
||||
LogUtils.d(TAG, "不是有效的MIDI文件");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 读取文件头信息(包含每拍ticks数)
|
||||
readHeaderInfo();
|
||||
|
||||
// 3. 解析每个轨道(包含事件的deltaTicks)
|
||||
MidiPlayer.MidiTrack[] tracks = new MidiPlayer.MidiTrack[mTrackCount];
|
||||
for (int i = 0; i < mTrackCount; i++) {
|
||||
tracks[i] = parseTrackWithTicks();
|
||||
}
|
||||
|
||||
// 返回解析结果(轨道数组 + 每拍ticks数)
|
||||
return new MidiPlayer.MidiParseResult(tracks, mTicksPerBeat);
|
||||
} finally {
|
||||
if (mInputStream != null) {
|
||||
mInputStream.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 原始解析方法(兼容旧逻辑)
|
||||
*/
|
||||
public MidiTrack[] parse() throws IOException {
|
||||
try {
|
||||
if (!verifyHeader()) {
|
||||
LogUtils.d(TAG, "不是有效的MIDI文件");
|
||||
return null;
|
||||
}
|
||||
|
||||
readHeaderInfo();
|
||||
MidiTrack[] tracks = new MidiTrack[mTrackCount];
|
||||
for (int i = 0; i < mTrackCount; i++) {
|
||||
tracks[i] = parseTrack();
|
||||
}
|
||||
return tracks;
|
||||
} finally {
|
||||
if (mInputStream != null) {
|
||||
mInputStream.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证MIDI文件头(必须以"MThd"开头)
|
||||
*/
|
||||
private boolean verifyHeader() throws IOException {
|
||||
byte[] header = new byte[4];
|
||||
int read = mInputStream.read(header);
|
||||
if (read != 4) {
|
||||
LogUtils.d(TAG, "文件头读取不完整,读取字节数: " + read);
|
||||
return false;
|
||||
}
|
||||
String headerStr = new String(header, US_ASCII);
|
||||
boolean isValid = "MThd".equals(headerStr);
|
||||
if (!isValid) {
|
||||
LogUtils.d(TAG, "无效的文件头标识: " + headerStr
|
||||
+ " (十六进制: " + bytesToHex(header) + ")");
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取MIDI文件头信息(提取每拍ticks数)
|
||||
*/
|
||||
private void readHeaderInfo() throws IOException {
|
||||
// 1. 读取头长度(4字节,标准MIDI固定为6)
|
||||
int headerLength = readInt();
|
||||
LogUtils.d(TAG, "MIDI文件头长度: " + headerLength);
|
||||
|
||||
// 2. 读取头数据(共6字节)
|
||||
byte[] headerData = new byte[6];
|
||||
int read = mInputStream.read(headerData);
|
||||
if (read != 6) {
|
||||
LogUtils.d(TAG, "文件头数据不完整,预期6字节,实际读取: " + read);
|
||||
throw new IOException("无效的MIDI文件头数据");
|
||||
}
|
||||
|
||||
// 3. 解析头信息(格式类型、轨道数、每拍ticks数)
|
||||
int formatType = ((headerData[0] & 0xFF) << 8) | (headerData[1] & 0xFF);
|
||||
mTrackCount = ((headerData[2] & 0xFF) << 8) | (headerData[3] & 0xFF);
|
||||
mTicksPerBeat = ((headerData[4] & 0xFF) << 8) | (headerData[5] & 0xFF); // 存储每拍ticks数
|
||||
|
||||
LogUtils.d(TAG, "MIDI文件格式: " + formatType);
|
||||
LogUtils.d(TAG, "时间分隔符(每拍 ticks): " + mTicksPerBeat);
|
||||
LogUtils.d(TAG, "解析到轨道数量: " + mTrackCount);
|
||||
|
||||
// 4. 处理扩展头
|
||||
if (headerLength > 6) {
|
||||
long skipped = mInputStream.skip(headerLength - 6);
|
||||
LogUtils.d(TAG, "跳过扩展头字节数: " + skipped);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单个轨道(包含事件的deltaTicks,用于速度控制)
|
||||
*/
|
||||
private MidiPlayer.MidiTrack parseTrackWithTicks() throws IOException {
|
||||
// 1. 读取轨道头(MTrk)
|
||||
byte[] trackHeader = new byte[4];
|
||||
int headerRead = mInputStream.read(trackHeader);
|
||||
if (headerRead != 4) {
|
||||
LogUtils.d(TAG, "轨道头读取不完整,实际读取: " + headerRead + "字节");
|
||||
return new MidiPlayer.MidiTrack(new ArrayList<MidiPlayer.MidiEvent>());
|
||||
}
|
||||
|
||||
// 2. 验证轨道头标识
|
||||
String headerStr = new String(trackHeader, US_ASCII);
|
||||
if (!"MTrk".equals(headerStr)) {
|
||||
LogUtils.d(TAG, "无效的轨道头标识: " + headerStr);
|
||||
return new MidiPlayer.MidiTrack(new ArrayList<MidiPlayer.MidiEvent>());
|
||||
}
|
||||
|
||||
// 3. 读取轨道长度
|
||||
int trackLength = readInt();
|
||||
LogUtils.d(TAG, "解析轨道,长度: " + trackLength + "字节");
|
||||
|
||||
// 4. 读取完整轨道数据
|
||||
byte[] trackData = new byte[trackLength];
|
||||
int totalRead = 0;
|
||||
while (totalRead < trackLength) {
|
||||
int bytesRead = mInputStream.read(trackData, totalRead, trackLength - totalRead);
|
||||
if (bytesRead == -1) {
|
||||
LogUtils.d(TAG, "轨道数据读取提前结束,已读取: " + totalRead);
|
||||
break;
|
||||
}
|
||||
totalRead += bytesRead;
|
||||
}
|
||||
|
||||
// 5. 解析轨道事件(包含deltaTicks)
|
||||
List<MidiPlayer.MidiEvent> events = new ArrayList<MidiPlayer.MidiEvent>();
|
||||
if (totalRead == trackLength) {
|
||||
parseEventsWithTicks(events, trackData);
|
||||
} else {
|
||||
LogUtils.d(TAG, "轨道数据不完整,跳过事件解析");
|
||||
}
|
||||
|
||||
return new MidiPlayer.MidiTrack(events);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析轨道事件(提取deltaTicks,用于计算播放延迟)
|
||||
*/
|
||||
private void parseEventsWithTicks(List<MidiPlayer.MidiEvent> events, byte[] trackData) {
|
||||
int offset = 0;
|
||||
while (offset < trackData.length) {
|
||||
// 1. 读取deltaTicks(事件间隔,单位ticks)
|
||||
long deltaTicks = readVariableLength(trackData, offset);
|
||||
int deltaSize = getVariableLengthSize(deltaTicks);
|
||||
offset += deltaSize;
|
||||
|
||||
if (offset >= trackData.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 2. 读取事件状态字节
|
||||
int statusByte = trackData[offset] & 0xFF;
|
||||
offset++;
|
||||
|
||||
// 3. 确定事件数据长度
|
||||
int dataLength = getEventDataLength(statusByte);
|
||||
if (offset + dataLength > trackData.length) {
|
||||
LogUtils.d(TAG, "事件数据不完整,状态字节: 0x" + Integer.toHexString(statusByte));
|
||||
break;
|
||||
}
|
||||
|
||||
// 4. 提取事件数据(状态字节+数据字节)
|
||||
byte[] eventData = new byte[1 + dataLength];
|
||||
eventData[0] = (byte) statusByte;
|
||||
System.arraycopy(trackData, offset, eventData, 1, dataLength);
|
||||
offset += dataLength;
|
||||
|
||||
// 5. 存储事件(包含deltaTicks)
|
||||
events.add(new MidiPlayer.MidiEvent(eventData, (int) deltaTicks));
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "轨道事件解析完成,事件数量: " + events.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 原始轨道解析方法(兼容旧逻辑)
|
||||
*/
|
||||
private MidiTrack parseTrack() throws IOException {
|
||||
// 1. 读取轨道头(MTrk)
|
||||
byte[] trackHeader = new byte[4];
|
||||
int headerRead = mInputStream.read(trackHeader);
|
||||
if (headerRead != 4) {
|
||||
LogUtils.d(TAG, "轨道头读取不完整,实际读取: " + headerRead + "字节");
|
||||
return new MidiTrack();
|
||||
}
|
||||
|
||||
// 2. 验证轨道头标识
|
||||
String headerStr = new String(trackHeader, US_ASCII);
|
||||
if (!"MTrk".equals(headerStr)) {
|
||||
LogUtils.d(TAG, "无效的轨道头标识: " + headerStr
|
||||
+ " (十六进制: " + bytesToHex(trackHeader) + ")");
|
||||
return new MidiTrack();
|
||||
}
|
||||
|
||||
// 3. 读取轨道长度
|
||||
int trackLength = readInt();
|
||||
LogUtils.d(TAG, "解析轨道,长度: " + trackLength + "字节");
|
||||
|
||||
// 4. 读取完整轨道数据
|
||||
byte[] trackData = new byte[trackLength];
|
||||
int totalRead = 0;
|
||||
while (totalRead < trackLength) {
|
||||
int bytesRead = mInputStream.read(trackData, totalRead, trackLength - totalRead);
|
||||
if (bytesRead == -1) {
|
||||
LogUtils.d(TAG, "轨道数据读取提前结束,已读取: " + totalRead + ",预期: " + trackLength);
|
||||
break;
|
||||
}
|
||||
totalRead += bytesRead;
|
||||
}
|
||||
|
||||
// 5. 解析轨道事件
|
||||
MidiTrack track = new MidiTrack();
|
||||
if (totalRead == trackLength) {
|
||||
parseEvents(track, trackData);
|
||||
} else {
|
||||
LogUtils.d(TAG, "轨道数据不完整,跳过事件解析");
|
||||
}
|
||||
|
||||
return track;
|
||||
}
|
||||
|
||||
/**
|
||||
* 原始事件解析方法(兼容旧逻辑)
|
||||
*/
|
||||
private void parseEvents(MidiTrack track, byte[] trackData) {
|
||||
int offset = 0;
|
||||
while (offset < trackData.length) {
|
||||
// 1. 读取可变长度时间戳(MIDI事件时间差)
|
||||
long deltaTime = readVariableLength(trackData, offset);
|
||||
offset += getVariableLengthSize(deltaTime);
|
||||
|
||||
if (offset >= trackData.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 2. 读取事件状态字节
|
||||
int statusByte = trackData[offset] & 0xFF;
|
||||
offset++;
|
||||
|
||||
// 3. 确定事件数据长度
|
||||
int dataLength = getEventDataLength(statusByte);
|
||||
if (offset + dataLength > trackData.length) {
|
||||
LogUtils.d(TAG, "事件数据不完整,状态字节: 0x" + Integer.toHexString(statusByte)
|
||||
+ ",剩余字节: " + (trackData.length - offset));
|
||||
break;
|
||||
}
|
||||
|
||||
// 4. 提取完整事件(状态字节+数据字节)
|
||||
byte[] event = new byte[1 + dataLength];
|
||||
event[0] = (byte) statusByte;
|
||||
System.arraycopy(trackData, offset, event, 1, dataLength);
|
||||
offset += dataLength;
|
||||
|
||||
track.addEvent(event);
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "轨道事件解析完成,事件数量: " + track.getEventCount());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据状态字节获取事件数据长度
|
||||
*/
|
||||
private int getEventDataLength(int statusByte) {
|
||||
int eventType = statusByte >> 4;
|
||||
switch (eventType) {
|
||||
case 0x8: // 音符关闭
|
||||
case 0x9: // 音符开启
|
||||
case 0xA: // 触后
|
||||
case 0xB: // 控制器
|
||||
case 0xE: // 弯音
|
||||
return 2;
|
||||
case 0xC: // 程序变更
|
||||
case 0xD: // 通道触后
|
||||
return 1;
|
||||
case 0xF: // 系统事件
|
||||
if (statusByte == 0xFF) { // 元事件
|
||||
return 1;
|
||||
} else if (statusByte == 0xF0 || statusByte == 0xF7) { // 系统专属事件
|
||||
return 0;
|
||||
}
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取可变长度整数(MIDI事件时间差deltaTicks)
|
||||
*/
|
||||
private long readVariableLength(byte[] data, int offset) {
|
||||
long value = 0;
|
||||
int b;
|
||||
int i = 0;
|
||||
do {
|
||||
b = data[offset + i] & 0xFF;
|
||||
value = (value << 7) | (b & 0x7F);
|
||||
i++;
|
||||
} while ((b & 0x80) != 0 && i < 4); // 最多4字节
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可变长度整数的字节数
|
||||
*/
|
||||
private int getVariableLengthSize(long value) {
|
||||
if (value < 0x80) return 1;
|
||||
if (value < 0x4000) return 2;
|
||||
if (value < 0x200000) return 3;
|
||||
return 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取无符号短整型(2字节,大端序)
|
||||
*/
|
||||
private int readUnsignedShort() throws IOException {
|
||||
int b1 = mInputStream.read() & 0xFF;
|
||||
int b2 = mInputStream.read() & 0xFF;
|
||||
return (b1 << 8) | b2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取整型(4字节,大端序)
|
||||
*/
|
||||
private int readInt() throws IOException {
|
||||
int b1 = mInputStream.read() & 0xFF;
|
||||
int b2 = mInputStream.read() & 0xFF;
|
||||
int b3 = mInputStream.read() & 0xFF;
|
||||
int b4 = mInputStream.read() & 0xFF;
|
||||
return (b1 << 24) | (b2 << 16) | (b3 << 8) | b4;
|
||||
}
|
||||
|
||||
/**
|
||||
* 字节数组转十六进制字符串(调试用)
|
||||
*/
|
||||
private String bytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02X ", b));
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 旧轨道类(兼容旧逻辑)
|
||||
*/
|
||||
/*public static class MidiTrack {
|
||||
private List<byte[]> events = new ArrayList<byte[]>();
|
||||
private boolean isMuted = false;
|
||||
private int currentIndex = 0;
|
||||
|
||||
public void addEvent(byte[] event) {
|
||||
events.add(event);
|
||||
}
|
||||
|
||||
public int getEventCount() {
|
||||
return events.size();
|
||||
}
|
||||
|
||||
public void setMute(boolean mute) {
|
||||
isMuted = mute;
|
||||
}
|
||||
|
||||
public boolean hasNextEvent() {
|
||||
return currentIndex < events.size();
|
||||
}
|
||||
|
||||
public byte[] nextEvent() {
|
||||
if (currentIndex < events.size()) {
|
||||
return events.get(currentIndex++);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
currentIndex = 0;
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
@@ -0,0 +1,655 @@
|
||||
package cc.winboll.studio.midiplayer;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@188.com>
|
||||
* @Date 2025/06/29 10:13
|
||||
* @Describe MidiPlayer
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.media.midi.MidiDevice;
|
||||
import android.media.midi.MidiDeviceInfo;
|
||||
import android.media.midi.MidiInputPort;
|
||||
import android.media.midi.MidiManager;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* MIDI播放器核心类,支持加载MIDI文件、连接合成器、控制播放轨道及事件解析
|
||||
* 需Android 6.0(API 23)及以上版本
|
||||
*/
|
||||
public class MidiPlayer {
|
||||
|
||||
public static final String TAG = "MidiPlayer";
|
||||
|
||||
// 上下文与系统服务
|
||||
private final Context mContext;
|
||||
private MidiManager mMidiManager;
|
||||
|
||||
// MIDI设备与端口
|
||||
private MidiDevice mMidiDevice;
|
||||
private MidiInputPort mInputPort;
|
||||
private boolean isSynthConnected = false;
|
||||
|
||||
// 线程与Handler
|
||||
private ExecutorService mExecutor;
|
||||
private final Handler mHandler = new Handler();
|
||||
|
||||
// 播放数据与状态
|
||||
private MidiTrack[] mTracks;
|
||||
private boolean isPlaying = false;
|
||||
private int mCurrentTrack = -1; // -1表示播放所有轨道
|
||||
private int mTicksPerBeat; // 每拍的ticks数(从MIDI文件解析)
|
||||
private long startTime; // 播放开始时间(用于日志)
|
||||
|
||||
// 音色库管理
|
||||
private final SoundFontManager mSoundFontManager;
|
||||
|
||||
// 连接回调接口
|
||||
public interface OnSynthConnectedListener {
|
||||
void onConnected(boolean success);
|
||||
}
|
||||
private OnSynthConnectedListener mConnectionListener;
|
||||
|
||||
|
||||
/**
|
||||
* MIDI事件类,存储事件数据及时间间隔
|
||||
*/
|
||||
public static class MidiEvent {
|
||||
public byte[] data; // 事件指令数据
|
||||
public int deltaTicks; // 与上一事件的时间间隔(ticks)
|
||||
|
||||
public MidiEvent(byte[] data, int deltaTicks) {
|
||||
this.data = data;
|
||||
this.deltaTicks = deltaTicks;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* MIDI轨道类,包含事件列表及播放状态
|
||||
*/
|
||||
public static class MidiTrack {
|
||||
private final List<MidiEvent> events;
|
||||
private boolean isMuted = false;
|
||||
private int currentIndex = 0;
|
||||
|
||||
public MidiTrack(List<MidiEvent> events) {
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
public boolean hasNextEvent() {
|
||||
return currentIndex < events.size();
|
||||
}
|
||||
|
||||
public MidiEvent nextEvent() {
|
||||
return currentIndex < events.size() ? events.get(currentIndex++) : null;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
currentIndex = 0;
|
||||
}
|
||||
|
||||
public void setMute(boolean mute) {
|
||||
isMuted = mute;
|
||||
}
|
||||
|
||||
public boolean isMuted() {
|
||||
return isMuted;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* MIDI解析结果类,包含轨道数组与每拍ticks数
|
||||
*/
|
||||
public static class MidiParseResult {
|
||||
public MidiTrack[] tracks;
|
||||
public int ticksPerBeat;
|
||||
|
||||
public MidiParseResult(MidiTrack[] tracks, int ticksPerBeat) {
|
||||
this.tracks = tracks;
|
||||
this.ticksPerBeat = ticksPerBeat;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 构造方法
|
||||
public MidiPlayer(Context context) {
|
||||
mContext = context;
|
||||
mSoundFontManager = new SoundFontManager(context);
|
||||
// 初始化MIDI服务(需API 23+)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mMidiManager = (MidiManager) context.getSystemService(Context.MIDI_SERVICE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 设置合成器连接回调
|
||||
public void setOnSynthConnectedListener(OnSynthConnectedListener listener) {
|
||||
mConnectionListener = listener;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 加载MIDI文件并解析为轨道
|
||||
* @param file MIDI文件
|
||||
* @return 是否加载成功
|
||||
*/
|
||||
public boolean loadMidiFile(File file) {
|
||||
try {
|
||||
MidiParser parser = new MidiParser(file);
|
||||
MidiParseResult result = parser.parseWithTicks();
|
||||
mTracks = result.tracks;
|
||||
mTicksPerBeat = result.ticksPerBeat;
|
||||
return mTracks != null && mTracks.length > 0;
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, "加载MIDI文件失败: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 加载音色库文件
|
||||
* @param soundFontFile 音色库文件
|
||||
* @return 是否加载成功
|
||||
*/
|
||||
public boolean loadSoundFont(File soundFontFile) {
|
||||
return mSoundFontManager.loadSoundFont(soundFontFile);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 开始播放MIDI文件
|
||||
*/
|
||||
public void start() {
|
||||
LogUtils.d(TAG, "start()");
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
LogUtils.d(TAG, "需Android 6.0及以上版本");
|
||||
notifyConnectionResult(false);
|
||||
return;
|
||||
}
|
||||
if (isPlaying || mTracks == null || mMidiManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
isPlaying = true;
|
||||
getExecutor().execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
LogUtils.d(TAG, "开始连接合成器");
|
||||
isSynthConnected = false;
|
||||
connectToSynth();
|
||||
|
||||
// 等待合成器连接(最多3秒)
|
||||
int waitCount = 0;
|
||||
while (!isSynthConnected && waitCount < 30) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
waitCount++;
|
||||
}
|
||||
|
||||
if (!isSynthConnected || mInputPort == null) {
|
||||
LogUtils.d(TAG, "合成器连接失败");
|
||||
isPlaying = false;
|
||||
notifyConnectionResult(false);
|
||||
return;
|
||||
}
|
||||
|
||||
notifyConnectionResult(true);
|
||||
playTracksWithSpeedControl();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 带速度控制的轨道播放逻辑
|
||||
*/
|
||||
private void playTracksWithSpeedControl() {
|
||||
LogUtils.d(TAG, "开始播放,每拍ticks数: " + mTicksPerBeat);
|
||||
if (mInputPort == null || mTracks == null || mTicksPerBeat <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
startTime = System.currentTimeMillis();
|
||||
int trackCount = mTracks.length;
|
||||
long[] trackNextEventTime = new long[trackCount]; // 各轨道下一事件的播放时间(ms)
|
||||
|
||||
// 初始化轨道状态
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
mTracks[i].reset();
|
||||
trackNextEventTime[i] = 0;
|
||||
}
|
||||
|
||||
// 默认BPM(可通过MIDI文件中的tempo事件动态调整)
|
||||
int bpm = 120;
|
||||
double msPerTick = (60.0 / bpm) * 1000 / mTicksPerBeat;
|
||||
|
||||
// 循环播放事件
|
||||
while (isPlaying) {
|
||||
// 找到最早需要播放的事件时间
|
||||
long earliestTime = Long.MAX_VALUE;
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
if ((mCurrentTrack == -1 || mCurrentTrack == i)
|
||||
&& !mTracks[i].isMuted()
|
||||
&& mTracks[i].hasNextEvent()) {
|
||||
earliestTime = Math.min(earliestTime, trackNextEventTime[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (earliestTime == Long.MAX_VALUE) {
|
||||
LogUtils.d(TAG, "所有轨道事件播放完毕");
|
||||
break;
|
||||
}
|
||||
|
||||
// 计算等待时间并休眠
|
||||
long currentTime = System.currentTimeMillis() - startTime;
|
||||
long delay = earliestTime - currentTime;
|
||||
if (delay > 0) {
|
||||
try {
|
||||
Thread.sleep(delay);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 播放所有到达时间点的事件
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
if ((mCurrentTrack == -1 || mCurrentTrack == i)
|
||||
&& !mTracks[i].isMuted()
|
||||
&& mTracks[i].hasNextEvent()
|
||||
&& trackNextEventTime[i] == earliestTime) {
|
||||
|
||||
MidiEvent event = mTracks[i].nextEvent();
|
||||
if (event != null) {
|
||||
try {
|
||||
logMidiEventDetails(i, event);
|
||||
mInputPort.send(event.data, 0, event.data.length);
|
||||
// 更新下一事件时间
|
||||
trackNextEventTime[i] = earliestTime + (long) (event.deltaTicks * msPerTick);
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, "发送事件失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isPlaying = false;
|
||||
LogUtils.d(TAG, "播放结束");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解析MIDI事件并输出详细日志
|
||||
*/
|
||||
private void logMidiEventDetails(int trackIndex, MidiEvent event) {
|
||||
// 无效事件增加原始数据日志,便于调试
|
||||
if (event.data == null || event.data.length < 2) {
|
||||
String dataStr = (event.data != null) ? Arrays.toString(event.data) : "null";
|
||||
LogUtils.d(TAG, "轨道[" + trackIndex + "] 无效事件: 数据长度不足 ("
|
||||
+ (event.data != null ? event.data.length : 0) + "字节),原始数据: " + dataStr);
|
||||
return;
|
||||
}
|
||||
|
||||
int statusByte = event.data[0] & 0xFF;
|
||||
int eventType = statusByte & 0xF0;
|
||||
int channel = (statusByte & 0x0F) + 1; // 通道号1-16
|
||||
|
||||
StringBuilder log = new StringBuilder();
|
||||
log.append("轨道[").append(trackIndex).append("] 事件类型: ");
|
||||
|
||||
switch (eventType) {
|
||||
case 0x90: // 音符开启
|
||||
if (event.data.length >= 3) {
|
||||
int pitch = event.data[1] & 0xFF;
|
||||
int velocity = event.data[2] & 0xFF;
|
||||
log.append("音符开启 | 通道: ").append(channel)
|
||||
.append(" | 音高: ").append(pitch).append(" (").append(getNoteName(pitch)).append(")")
|
||||
.append(" | 力度: ").append(velocity)
|
||||
.append(" | 间隔ticks: ").append(event.deltaTicks)
|
||||
.append(" | 播放时间: ").append(System.currentTimeMillis() - startTime).append("ms");
|
||||
} else {
|
||||
log.append("音符开启 (数据不完整) | 长度: ").append(event.data.length);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x80: // 音符关闭
|
||||
if (event.data.length >= 3) {
|
||||
int pitch = event.data[1] & 0xFF;
|
||||
int velocity = event.data[2] & 0xFF;
|
||||
log.append("音符关闭 | 通道: ").append(channel)
|
||||
.append(" | 音高: ").append(pitch).append(" (").append(getNoteName(pitch)).append(")")
|
||||
.append(" | 力度: ").append(velocity)
|
||||
.append(" | 间隔ticks: ").append(event.deltaTicks)
|
||||
.append(" | 播放时间: ").append(System.currentTimeMillis() - startTime).append("ms");
|
||||
} else {
|
||||
log.append("音符关闭 (数据不完整) | 长度: ").append(event.data.length);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xB0: // 控制变化
|
||||
if (event.data.length >= 3) {
|
||||
int controlNumber = event.data[1] & 0xFF;
|
||||
int controlValue = event.data[2] & 0xFF;
|
||||
log.append("控制变化 | 通道: ").append(channel)
|
||||
.append(" | 控制器: ").append(controlNumber).append(" (").append(getControlName(controlNumber)).append(")")
|
||||
.append(" | 数值: ").append(controlValue);
|
||||
} else {
|
||||
log.append("控制变化 (数据不完整) | 长度: ").append(event.data.length);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xC0: // 程序改变(乐器切换)
|
||||
if (event.data.length >= 2) {
|
||||
int program = event.data[1] & 0xFF;
|
||||
// 增加乐器编号有效性校验日志
|
||||
String instrumentName = getInstrumentName(program);
|
||||
if (program < 0 || program >= 128) {
|
||||
instrumentName += " (超出GM标准范围0-127)";
|
||||
}
|
||||
log.append("程序改变 | 通道: ").append(channel)
|
||||
.append(" | 乐器编号: ").append(program).append(" (").append(instrumentName).append(")");
|
||||
} else {
|
||||
log.append("程序改变 (数据不完整) | 长度: ").append(event.data.length);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xD0: // 通道触后事件
|
||||
log.append("通道触后 | 通道: ").append(channel)
|
||||
.append(" | 压力值: ").append(event.data[1] & 0xFF);
|
||||
break;
|
||||
|
||||
case 0xF0: // 系统专属事件(SysEx)
|
||||
log.append("系统专属事件(SysEx) | 长度: ").append(event.data.length)
|
||||
.append(" | 首字节: 0x").append(Integer.toHexString(event.data[0] & 0xFF));
|
||||
break;
|
||||
|
||||
default:
|
||||
log.append("未知类型 (0x").append(Integer.toHexString(eventType)).append(") | 通道: ").append(channel)
|
||||
.append(",原始数据: ").append(Arrays.toString(event.data));
|
||||
break;
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, log.toString());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 音高转音符名称(如60→C4)
|
||||
*/
|
||||
private String getNoteName(int pitch) {
|
||||
if (pitch < 0 || pitch > 127) return "无效音高";
|
||||
String[] noteNames = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
|
||||
int octave = (pitch / 12) - 1; // C4对应60
|
||||
return noteNames[pitch % 12] + octave;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* MIDI控制器编号转名称
|
||||
*/
|
||||
private String getControlName(int controlNumber) {
|
||||
switch (controlNumber) {
|
||||
case 0: return "Bank Select (MSB)";
|
||||
case 1: return "Modulation Wheel";
|
||||
case 7: return "音量控制";
|
||||
case 10: return "声像控制";
|
||||
case 11: return "表达控制";
|
||||
case 64: return "延音踏板";
|
||||
case 121: return "重置所有控制器";
|
||||
default: return "未知控制器";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 乐器编号转名称(GM标准)
|
||||
*/
|
||||
private String getInstrumentName(int program) {
|
||||
String[] instruments = {
|
||||
"钢琴", "明亮钢琴", "电钢琴", "Honky-tonk钢琴", "电钢琴", "羽管键琴",
|
||||
"击弦古钢琴", "颤音琴", "竖琴", "管风琴", "手风琴", "acoustic贝斯",
|
||||
"电贝斯(指弹)", "电贝斯(拨片)", "小提琴", "大提琴"
|
||||
};
|
||||
if (program < 0 || program >= 128) {
|
||||
return "无效编号";
|
||||
}
|
||||
if (program < instruments.length) {
|
||||
return instruments[program];
|
||||
} else {
|
||||
return "乐器 " + program;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 连接MIDI合成器
|
||||
*/
|
||||
public void connectToSynth() {
|
||||
if (mMidiManager == null) return;
|
||||
closeMidiResources();
|
||||
searchSynthInDevices();
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索并选择可用的MIDI设备
|
||||
*/
|
||||
private void searchSynthInDevices() {
|
||||
MidiDeviceInfo[] devices = mMidiManager.getDevices();
|
||||
if (devices == null || devices.length == 0) {
|
||||
LogUtils.d(TAG, "未检测到MIDI设备");
|
||||
isSynthConnected = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 优先选择合成器类型(3),其次选择有输入端口的设备
|
||||
MidiDeviceInfo targetDevice = null;
|
||||
for (MidiDeviceInfo device : devices) {
|
||||
if (device.getType() == 3) {
|
||||
targetDevice = device;
|
||||
break;
|
||||
}
|
||||
if (targetDevice == null && device.getInputPortCount() > 0) {
|
||||
targetDevice = device;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetDevice != null) {
|
||||
LogUtils.d(TAG, "尝试打开设备: 设备ID=" + targetDevice.getId() + ",名称=" + targetDevice.getProperties().getString(MidiDeviceInfo.PROPERTY_NAME));
|
||||
mMidiManager.openDevice(targetDevice, new MidiManager.OnDeviceOpenedListener() {
|
||||
@Override
|
||||
public void onDeviceOpened(MidiDevice device) {
|
||||
setupMidiDevice(device);
|
||||
}
|
||||
}, mHandler);
|
||||
} else {
|
||||
LogUtils.d(TAG, "未找到可用设备(无合成器或带输入端口的设备)");
|
||||
isSynthConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 初始化MIDI设备并打开输入端口
|
||||
*/
|
||||
private void setupMidiDevice(MidiDevice device) {
|
||||
if (device == null) {
|
||||
LogUtils.d(TAG, "打开MIDI设备失败: 设备为null");
|
||||
isSynthConnected = false;
|
||||
return;
|
||||
}
|
||||
|
||||
mMidiDevice = device;
|
||||
try {
|
||||
// 从设备信息中获取输入端口数量
|
||||
MidiDeviceInfo deviceInfo = device.getInfo();
|
||||
if (deviceInfo.getInputPortCount() > 0) {
|
||||
mInputPort = device.openInputPort(0);
|
||||
isSynthConnected = (mInputPort != null);
|
||||
LogUtils.d(TAG, isSynthConnected ? "成功打开输入端口(端口0)" : "输入端口为null,打开失败");
|
||||
} else {
|
||||
LogUtils.d(TAG, "设备无输入端口,无法接收MIDI事件");
|
||||
isSynthConnected = false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "打开端口失败: " + e.getMessage());
|
||||
isSynthConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 关闭MIDI设备及端口资源
|
||||
*/
|
||||
private void closeMidiResources() {
|
||||
if (mInputPort != null) {
|
||||
try {
|
||||
mInputPort.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, "关闭输入端口失败: " + e.getMessage());
|
||||
}
|
||||
mInputPort = null;
|
||||
}
|
||||
if (mMidiDevice != null) {
|
||||
try {
|
||||
mMidiDevice.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, "关闭MIDI设备失败: " + e.getMessage());
|
||||
}
|
||||
mMidiDevice = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通知合成器连接结果
|
||||
*/
|
||||
private void notifyConnectionResult(final boolean success) {
|
||||
if (mConnectionListener != null) {
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mConnectionListener.onConnected(success);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置指定轨道静音状态
|
||||
* @param trackIndex 轨道索引
|
||||
* @param mute 是否静音
|
||||
*/
|
||||
public void setTrackMute(int trackIndex, boolean mute) {
|
||||
if (mTracks == null) {
|
||||
LogUtils.d(TAG, "设置静音失败:未加载MIDI轨道");
|
||||
return;
|
||||
}
|
||||
if (trackIndex >= 0 && trackIndex < mTracks.length) {
|
||||
mTracks[trackIndex].setMute(mute);
|
||||
LogUtils.d(TAG, "轨道[" + trackIndex + "] 静音状态: " + (mute ? "已静音" : "正常播放"));
|
||||
} else {
|
||||
LogUtils.d(TAG, "设置静音失败:无效轨道索引 " + trackIndex + "(总轨道数:" + mTracks.length + ")");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 切换当前播放轨道(-1表示所有轨道)
|
||||
* @param trackIndex 轨道索引
|
||||
*/
|
||||
public void setCurrentTrack(int trackIndex) {
|
||||
if (mTracks == null) {
|
||||
LogUtils.d(TAG, "切换轨道失败:未加载MIDI轨道");
|
||||
return;
|
||||
}
|
||||
// 校验轨道索引有效性
|
||||
if (trackIndex == -1 || (trackIndex >= 0 && trackIndex < mTracks.length)) {
|
||||
mCurrentTrack = trackIndex;
|
||||
LogUtils.d(TAG, "切换当前播放轨道: " + (trackIndex == -1 ? "所有轨道" : "轨道[" + trackIndex + "]"));
|
||||
} else {
|
||||
LogUtils.d(TAG, "切换轨道失败:无效轨道索引 " + trackIndex + "(总轨道数:" + mTracks.length + ")");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取轨道总数
|
||||
* @return 轨道数量
|
||||
*/
|
||||
public int getTrackCount() {
|
||||
return mTracks != null ? mTracks.length : 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 暂停播放
|
||||
*/
|
||||
public void pause() {
|
||||
boolean wasPlaying = isPlaying;
|
||||
isPlaying = false;
|
||||
LogUtils.d(TAG, "播放暂停: " + (wasPlaying ? "已暂停当前播放" : "当前未在播放"));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 停止播放并释放所有资源
|
||||
*/
|
||||
public void stop() {
|
||||
boolean wasPlaying = isPlaying;
|
||||
isPlaying = false;
|
||||
isSynthConnected = false;
|
||||
closeMidiResources();
|
||||
if (mExecutor != null) {
|
||||
mExecutor.shutdownNow();
|
||||
mExecutor = null;
|
||||
}
|
||||
LogUtils.d(TAG, "播放停止: " + (wasPlaying ? "已终止播放并释放资源" : "当前未在播放"));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取线程池(单例)
|
||||
*/
|
||||
private ExecutorService getExecutor() {
|
||||
if (mExecutor == null || mExecutor.isShutdown() || mExecutor.isTerminated()) {
|
||||
mExecutor = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
return mExecutor;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 检查合成器是否已连接
|
||||
* @return 连接状态
|
||||
*/
|
||||
public boolean isSynthConnected() {
|
||||
return isSynthConnected;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取当前MIDI输入端口
|
||||
* @return 输入端口
|
||||
*/
|
||||
public MidiInputPort getInputPort() {
|
||||
return mInputPort;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
package cc.winboll.studio.midiplayer;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@188.com>
|
||||
* @Date 2025/06/29 10:10
|
||||
* @Describe Midi 播放窗口
|
||||
*/
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.media.midi.MidiInputPort;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.ListView;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import com.hjq.toast.ToastUtils;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MidiPlayerActivity extends WinBoLLActivity {
|
||||
|
||||
public static final String TAG = "MidiPlayerActivity";
|
||||
|
||||
private MidiPlayer mMidiPlayer;
|
||||
private Button mPlayBtn, mPauseBtn, mStopBtn, mTestBtn;
|
||||
private ListView mTrackListView;
|
||||
private ListView mFileListView;
|
||||
private TrackAdapter mTrackAdapter;
|
||||
private TextView mFileNameTv;
|
||||
private List<File> mMidiFileList = new ArrayList<>();
|
||||
|
||||
@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_midi_player);
|
||||
|
||||
// 初始化控件
|
||||
mPlayBtn = (Button) findViewById(R.id.btn_play);
|
||||
mPauseBtn = (Button) findViewById(R.id.btn_pause);
|
||||
mStopBtn = (Button) findViewById(R.id.btn_stop);
|
||||
mTestBtn = (Button) findViewById(R.id.btn_test);
|
||||
mTrackListView = (ListView) findViewById(R.id.lv_tracks);
|
||||
mFileListView = (ListView) findViewById(R.id.lv_midi_files);
|
||||
mFileNameTv = (TextView) findViewById(R.id.tv_file_name);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
initMidiPlayer();
|
||||
loadMidiFileList();
|
||||
initControlButtons();
|
||||
initTestButton();
|
||||
} else {
|
||||
mFileNameTv.setText("当前设备不支持MIDI播放(需Android 6.0及以上)");
|
||||
disableButtons();
|
||||
}
|
||||
|
||||
copyAssetsMidiFiles();
|
||||
}
|
||||
|
||||
// 初始化测试按钮及1分钟测试逻辑
|
||||
private void initTestButton() {
|
||||
mTestBtn.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
testMidiOutput();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 1分钟MIDI测试序列:包含多样音符组合,验证输出功能
|
||||
*/
|
||||
private void testMidiOutput() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
ToastUtils.show("测试需要Android 6.0及以上");
|
||||
return;
|
||||
}
|
||||
|
||||
if (mMidiPlayer == null) {
|
||||
initMidiPlayer();
|
||||
}
|
||||
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// 确保合成器连接
|
||||
if (!mMidiPlayer.isSynthConnected()) {
|
||||
LogUtils.d(TAG, "测试:连接合成器中...");
|
||||
mMidiPlayer.connectToSynth();
|
||||
|
||||
// 最多等待3秒连接
|
||||
int waitCount = 0;
|
||||
while (!mMidiPlayer.isSynthConnected() && waitCount < 30) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
waitCount++;
|
||||
}
|
||||
}
|
||||
|
||||
final MidiInputPort inputPort = mMidiPlayer.getInputPort();
|
||||
if (inputPort == null) {
|
||||
LogUtils.d(TAG, "测试失败:无可用输入端口");
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ToastUtils.show("测试失败:未找到输入端口");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ToastUtils.show("开始1分钟测试音符...");
|
||||
}
|
||||
});
|
||||
|
||||
// 测试序列定义:[音高, 力度, 时长(毫秒)]
|
||||
int[][] noteSequence = {
|
||||
// 0-10秒:低音区短音符
|
||||
{48, 64, 200}, {50, 64, 200}, {52, 64, 200}, {53, 64, 200},
|
||||
{55, 64, 200}, {57, 64, 200}, {59, 64, 200}, {60, 64, 400},
|
||||
|
||||
// 10-20秒:中音区连音
|
||||
{60, 72, 300}, {62, 72, 300}, {64, 72, 300}, {65, 72, 300},
|
||||
{67, 72, 300}, {69, 72, 300}, {71, 72, 300}, {72, 72, 600},
|
||||
|
||||
// 20-30秒:高音区跳音
|
||||
{72, 80, 150}, {76, 80, 150}, {79, 80, 150}, {84, 80, 300},
|
||||
{79, 80, 150}, {76, 80, 150}, {72, 80, 150}, {69, 80, 450},
|
||||
|
||||
// 30-40秒:和弦组合
|
||||
{60, 64, 400}, {64, 64, 400}, {67, 64, 400}, // C和弦
|
||||
{62, 64, 400}, {65, 64, 400}, {69, 64, 400}, // Dm和弦
|
||||
{64, 64, 400}, {67, 64, 400}, {71, 64, 400}, // Em和弦
|
||||
|
||||
// 40-50秒:渐强长音
|
||||
{55, 40, 1000}, {55, 60, 1000}, {55, 80, 1000}, {55, 100, 1000},
|
||||
|
||||
// 50-60秒:快速音阶
|
||||
{50, 70, 100}, {52, 70, 100}, {53, 70, 100}, {55, 70, 100},
|
||||
{57, 70, 100}, {59, 70, 100}, {60, 70, 100}, {62, 70, 100},
|
||||
{64, 70, 100}, {65, 70, 100}, {67, 70, 100}, {69, 70, 100},
|
||||
{71, 70, 100}, {72, 90, 500}
|
||||
};
|
||||
|
||||
// 发送测试序列
|
||||
for (int[] note : noteSequence) {
|
||||
if (!mMidiPlayer.isSynthConnected()) {
|
||||
LogUtils.d(TAG, "连接已断开,停止测试");
|
||||
break;
|
||||
}
|
||||
|
||||
int pitch = note[0];
|
||||
int velocity = note[1];
|
||||
int duration = note[2];
|
||||
|
||||
// 发送音符开启事件
|
||||
byte[] noteOn = new byte[]{(byte) 0x90, (byte) pitch, (byte) velocity};
|
||||
inputPort.send(noteOn, 0, noteOn.length);
|
||||
//LogUtils.d(TAG, "测试音符:音高=" + pitch + ", 力度=" + velocity + ", 时长=" + duration + "ms");
|
||||
|
||||
// 等待音符时长
|
||||
Thread.sleep(duration);
|
||||
|
||||
// 发送音符关闭事件
|
||||
byte[] noteOff = new byte[]{(byte) 0x80, (byte) pitch, 0};
|
||||
inputPort.send(noteOff, 0, noteOff.length);
|
||||
}
|
||||
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ToastUtils.show("1分钟测试完成");
|
||||
}
|
||||
});
|
||||
|
||||
} catch (final Exception e) {
|
||||
LogUtils.d(TAG, "测试出错:" + e.getMessage());
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ToastUtils.show("测试出错:" + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
// 拷贝Assets中的MIDI文件到本地
|
||||
private void copyAssetsMidiFiles() {
|
||||
final AssetMidiCopier copier = new AssetMidiCopier(this);
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final boolean success = copier.copyMidiFiles();
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (success) {
|
||||
File midiDir = copier.getMidiTargetDir();
|
||||
LogUtils.d(TAG, "MIDI文件拷贝成功,路径:" + midiDir.getAbsolutePath());
|
||||
// 重新加载文件列表
|
||||
loadMidiFileList();
|
||||
} else {
|
||||
ToastUtils.show("MIDI文件拷贝失败");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
// 初始化MIDI播放器
|
||||
private void initMidiPlayer() {
|
||||
mMidiPlayer = new MidiPlayer(this);
|
||||
mMidiPlayer.setOnSynthConnectedListener(new MidiPlayer.OnSynthConnectedListener() {
|
||||
@Override
|
||||
public void onConnected(final boolean success) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (success) {
|
||||
ToastUtils.show("MIDI合成器连接成功");
|
||||
} else {
|
||||
ToastUtils.show("未找到MIDI合成器,请安装第三方合成器应用");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 加载本地MIDI文件列表
|
||||
private void loadMidiFileList() {
|
||||
File midiDir = new File(getFilesDir(), "midi");
|
||||
if (!midiDir.exists()) {
|
||||
midiDir.mkdirs();
|
||||
mFileNameTv.setText("midi目录已创建,等待文件拷贝...");
|
||||
return;
|
||||
}
|
||||
|
||||
mMidiFileList.clear();
|
||||
File[] files = midiDir.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
String name = file.getName().toLowerCase();
|
||||
if (name.endsWith(".mid") || name.endsWith(".midi")) {
|
||||
mMidiFileList.add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mMidiFileList.isEmpty()) {
|
||||
mFileNameTv.setText("midi目录中无可用文件");
|
||||
} else {
|
||||
showFileList();
|
||||
mFileNameTv.setText("找到" + mMidiFileList.size() + "个MIDI文件");
|
||||
}
|
||||
}
|
||||
|
||||
// 显示MIDI文件列表
|
||||
private void showFileList() {
|
||||
List<String> fileNameList = new ArrayList<>();
|
||||
for (File file : mMidiFileList) {
|
||||
fileNameList.add(file.getName());
|
||||
}
|
||||
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
|
||||
this,
|
||||
android.R.layout.simple_list_item_1,
|
||||
fileNameList
|
||||
);
|
||||
mFileListView.setAdapter(adapter);
|
||||
|
||||
mFileListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
mMidiPlayer.stop();
|
||||
File selectedFile = mMidiFileList.get(position);
|
||||
boolean loaded = mMidiPlayer.loadMidiFile(selectedFile);
|
||||
if (loaded) {
|
||||
mFileNameTv.setText("当前文件:" + selectedFile.getName());
|
||||
initTrackList();
|
||||
ToastUtils.show("已加载:" + selectedFile.getName());
|
||||
} else {
|
||||
mFileNameTv.setText("文件加载失败:" + selectedFile.getName());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化轨道列表
|
||||
private void initTrackList() {
|
||||
mTrackAdapter = new TrackAdapter(this, mMidiPlayer);
|
||||
mTrackListView.setAdapter(mTrackAdapter);
|
||||
}
|
||||
|
||||
// 初始化播放控制按钮
|
||||
private void initControlButtons() {
|
||||
mPlayBtn.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mMidiPlayer != null) {
|
||||
mMidiPlayer.start();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mPauseBtn.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mMidiPlayer != null) {
|
||||
mMidiPlayer.pause();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mStopBtn.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mMidiPlayer != null) {
|
||||
mMidiPlayer.stop();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 禁用所有按钮(低版本设备)
|
||||
private void disableButtons() {
|
||||
mPlayBtn.setEnabled(false);
|
||||
mPauseBtn.setEnabled(false);
|
||||
mStopBtn.setEnabled(false);
|
||||
mTestBtn.setEnabled(false);
|
||||
}
|
||||
|
||||
// 轨道列表适配器
|
||||
private class TrackAdapter extends android.widget.BaseAdapter {
|
||||
private Context mContext;
|
||||
private MidiPlayer mPlayer;
|
||||
|
||||
public TrackAdapter(Context context, MidiPlayer player) {
|
||||
mContext = context;
|
||||
mPlayer = player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mPlayer != null ? mPlayer.getTrackCount() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(final int position, View convertView, ViewGroup parent) {
|
||||
View view = getLayoutInflater().inflate(R.layout.item_track, parent, false);
|
||||
TextView trackNameTv = (TextView) view.findViewById(R.id.tv_track_name);
|
||||
final Button muteBtn = (Button) view.findViewById(R.id.btn_mute);
|
||||
SeekBar volumeSb = (SeekBar) view.findViewById(R.id.sb_volume);
|
||||
|
||||
trackNameTv.setText("轨道 " + (position + 1));
|
||||
muteBtn.setText("静音");
|
||||
|
||||
// 静音按钮逻辑
|
||||
muteBtn.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
boolean isMuted = muteBtn.isSelected();
|
||||
mPlayer.setTrackMute(position, !isMuted);
|
||||
muteBtn.setSelected(!isMuted);
|
||||
muteBtn.setText(isMuted ? "静音" : "已静音");
|
||||
}
|
||||
});
|
||||
|
||||
// 音量条(预留逻辑)
|
||||
volumeSb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
// 可添加音量控制逻辑(如发送MIDI音量事件)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
// 日志查看按钮点击事件
|
||||
public void onLog(View view) {
|
||||
App.getWinBoLLActivityManager().startLogActivity(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (mMidiPlayer != null) {
|
||||
mMidiPlayer.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package cc.winboll.studio.midiplayer;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@188.com>
|
||||
* @Date 2025/06/29 10:24
|
||||
* @Describe Midi轨道类,用于存储和管理单条Midi轨道的事件
|
||||
*/
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class MidiTrack {
|
||||
public static final String TAG = "MidiTrack";
|
||||
|
||||
// 轨道事件列表(存储Midi事件字节数组)
|
||||
private List<byte[]> mEvents = new ArrayList<>();
|
||||
// 事件迭代器(用于播放时遍历事件)
|
||||
private Iterator<byte[]> mEventIterator;
|
||||
// 轨道是否静音
|
||||
private boolean isMuted = false;
|
||||
|
||||
public MidiTrack() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加Midi事件到轨道
|
||||
*/
|
||||
public void addEvent(byte[] event) {
|
||||
mEvents.add(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置轨道播放状态(回到起始位置)
|
||||
*/
|
||||
public void reset() {
|
||||
mEventIterator = mEvents.iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否有下一个事件
|
||||
*/
|
||||
public boolean hasNextEvent() {
|
||||
// 如果静音,返回false不播放事件
|
||||
if (isMuted) {
|
||||
return false;
|
||||
}
|
||||
// 初始化迭代器(首次使用或重置后)
|
||||
if (mEventIterator == null) {
|
||||
reset();
|
||||
}
|
||||
return mEventIterator.hasNext();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一个Midi事件
|
||||
*/
|
||||
public byte[] nextEvent() {
|
||||
if (mEventIterator == null) {
|
||||
reset();
|
||||
}
|
||||
return mEventIterator.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置轨道静音状态
|
||||
*/
|
||||
public void setMute(boolean mute) {
|
||||
isMuted = mute;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取轨道事件数量
|
||||
*/
|
||||
public int getEventCount() {
|
||||
return mEvents.size();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package cc.winboll.studio.midiplayer;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@188.com>
|
||||
* @Date 2025/06/29 10:12
|
||||
* @Describe SoundFontManager
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
// 简化版:实际需使用SoundFont解析库(如Audiokit、FluidSynth)
|
||||
public class SoundFontManager {
|
||||
|
||||
public static final String TAG = "SoundFontManager";
|
||||
|
||||
private Context mContext;
|
||||
private File mSoundFontFile;
|
||||
|
||||
public SoundFontManager(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
// 加载SoundFont文件(.sf2格式)
|
||||
public boolean loadSoundFont(File file) {
|
||||
if (!file.exists() || !file.getName().endsWith(".sf2")) {
|
||||
LogUtils.d(TAG, "无效的SoundFont文件");
|
||||
return false;
|
||||
}
|
||||
// 复制到应用私有目录(可选)
|
||||
try {
|
||||
File dest = new File(mContext.getFilesDir(), "custom_soundfont.sf2");
|
||||
FileInputStream fis = new FileInputStream(file);
|
||||
FileOutputStream fos = new FileOutputStream(dest);
|
||||
byte[] buffer = new byte[1024];
|
||||
int len;
|
||||
while ((len = fis.read(buffer)) != -1) {
|
||||
fos.write(buffer, 0, len);
|
||||
}
|
||||
fis.close();
|
||||
fos.close();
|
||||
mSoundFontFile = dest;
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, "复制SoundFont失败: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取加载的SoundFont路径(供合成器使用)
|
||||
public String getSoundFontPath() {
|
||||
return mSoundFontFile != null ? mSoundFontFile.getAbsolutePath() : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package cc.winboll.studio.midiplayer;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@188.com>
|
||||
* @Date 2025/06/29 10:40
|
||||
* @Describe WinBoLLActivity
|
||||
*/
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
|
||||
|
||||
public class WinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
|
||||
|
||||
public static final String TAG = "WinBoLLActivity";
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
LogUtils.d(TAG, String.format("onResume %s", getTag()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
/*if (item.getItemId() == R.id.item_log) {
|
||||
GlobalApplication.getWinBoLLActivityManager().startLogActivity(this);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.item_home) {
|
||||
GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), MainActivity.class);
|
||||
return true;
|
||||
}*/
|
||||
// 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
GlobalApplication.getWinBoLLActivityManager().add(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
GlobalApplication.getWinBoLLActivityManager().registeRemove(this);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
170
midiplayer/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="#26A69A"
|
||||
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>
|
||||
57
midiplayer/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,57 @@
|
||||
<?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="MidiPlayer"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"/>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="OpenMidiPlayer"
|
||||
android:onClick="onOpenMidiPlayer"/>
|
||||
|
||||
</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>
|
||||
|
||||
91
midiplayer/src/main/res/layout/activity_midi_player.xml
Normal file
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:text="MIDI文件列表"
|
||||
android:background="@android:color/darker_gray"/>
|
||||
|
||||
<ListView
|
||||
android:id="@+id/lv_midi_files"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_file_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:text="未选择文件"
|
||||
android:background="@android:color/holo_blue_light"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:text="轨道控制"
|
||||
android:background="@android:color/darker_gray"/>
|
||||
|
||||
<ListView
|
||||
android:id="@+id/lv_tracks"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_play"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="播放"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_pause"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="暂停"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_stop"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="停止"/>
|
||||
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_test"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="测试音符"
|
||||
android:layout_below="@id/btn_stop"/>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Log"
|
||||
android:onClick="onLog"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
37
midiplayer/src/main/res/layout/item_track.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_track_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="轨道 1" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_mute"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="静音" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_solo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:text="独奏" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/sb_volume"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:max="100"
|
||||
android:progress="80" />
|
||||
</LinearLayout>
|
||||
|
||||
@@ -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
midiplayer/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
midiplayer/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
midiplayer/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
midiplayer/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
midiplayer/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
midiplayer/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
midiplayer/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
midiplayer/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
midiplayer/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
midiplayer/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
6
midiplayer/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
midiplayer/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<resources>
|
||||
<string name="app_name">MidiPlayer</string>
|
||||
|
||||
</resources>
|
||||
11
midiplayer/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
midiplayer/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
midiplayer/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>
|
||||
@@ -46,7 +46,7 @@ android {
|
||||
dependencies {
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
api 'cc.winboll.studio:libaes:15.9.3'
|
||||
api 'cc.winboll.studio:libapputils:15.8.5'
|
||||
api 'cc.winboll.studio:libapputils:15.8.6'
|
||||
api 'cc.winboll.studio:libappbase:15.9.5'
|
||||
|
||||
api 'io.github.medyo:android-about-page:2.0.0'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sun Aug 31 06:13:45 CST 2025
|
||||
stageCount=7
|
||||
#Sat Sep 06 01:57:20 HKT 2025
|
||||
stageCount=9
|
||||
libraryProject=
|
||||
baseVersion=15.3
|
||||
publishVersion=15.3.6
|
||||
publishVersion=15.3.8
|
||||
buildCount=0
|
||||
baseBetaVersion=15.3.7
|
||||
baseBetaVersion=15.3.9
|
||||
|
||||
@@ -57,6 +57,7 @@ public class ComposeSMSActivity extends BaseActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate");
|
||||
setContentView(R.layout.activity_composesms);
|
||||
|
||||
// 初始化Intent数据(增加空判断,避免NullPointerException)
|
||||
@@ -116,8 +117,9 @@ public class ComposeSMSActivity extends BaseActivity {
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化联系人列表
|
||||
// 初始化联系人列表(关键:设置单选模式,确保选中状态生效)
|
||||
mlvContracts = (ListView) findViewById(R.id.activitycomposesmsListView1);
|
||||
mlvContracts.setChoiceMode(ListView.CHOICE_MODE_SINGLE); // 开启单选,与布局中一致
|
||||
|
||||
// 初始化号码输入框(核心:优化文本变化监听逻辑)
|
||||
metTO = (EditText) findViewById(R.id.activitycomposesmsEditText1);
|
||||
@@ -171,7 +173,7 @@ public class ComposeSMSActivity extends BaseActivity {
|
||||
}
|
||||
}
|
||||
|
||||
// 核心优化:根据输入号码筛选列表(无结果则显示空列表)
|
||||
// 核心优化:根据输入号码筛选列表(无结果则显示空列表,优化选中逻辑)
|
||||
private void filterListByPhone(String inputPhone) {
|
||||
PhoneUtil phoneUtil = new PhoneUtil(this);
|
||||
List<PhoneBean> allContacts = phoneUtil.getPhoneList();
|
||||
@@ -190,10 +192,29 @@ public class ComposeSMSActivity extends BaseActivity {
|
||||
// 用筛选结果更新列表(无结果则传入空列表)
|
||||
initAdapter(matchedContacts.isEmpty() ? new ArrayList<PhoneBean>() : matchedContacts);
|
||||
|
||||
// 定位到第一个匹配项(如果有)
|
||||
// 定位并选中匹配项(如果有)
|
||||
if (!matchedContacts.isEmpty()) {
|
||||
mlvContracts.setSelection(0);
|
||||
mtvTOName.setText(matchedContacts.get(0).getName());
|
||||
boolean isFound = false;
|
||||
for (int i = 0; i < matchedContacts.size(); i++) {
|
||||
PhoneBean item = matchedContacts.get(i);
|
||||
// 精确匹配号码(兼容区域码格式)
|
||||
if (phoneUtil.isTheSamePhoneNumber(item.getTelPhone(), inputPhone)) {
|
||||
mtvTOName.setText(item.getName());
|
||||
// 关键:先滚动到目标位置,再设置选中状态
|
||||
mlvContracts.setSelection(i);
|
||||
// 主动设置选中(确保样式生效,兼容部分系统)
|
||||
mlvContracts.setItemChecked(i, true);
|
||||
LogUtils.d(TAG, String.format("%s 匹配 %s,选中位置:%d", inputPhone, item.getTelPhone(), i));
|
||||
isFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 若未精确匹配,选中第一个结果
|
||||
/*if (!isFound) {
|
||||
mlvContracts.setSelection(0);
|
||||
mlvContracts.setItemChecked(0, true);
|
||||
mtvTOName.setText(matchedContacts.get(0).getName());
|
||||
}*/
|
||||
} else {
|
||||
mtvTOName.setText(""); // 无结果时清空姓名显示
|
||||
}
|
||||
@@ -206,7 +227,9 @@ public class ComposeSMSActivity extends BaseActivity {
|
||||
List<PhoneBean> matchedContacts = phoneUtil.getPhonesByName(searchName);
|
||||
initAdapter(matchedContacts);
|
||||
if (!matchedContacts.isEmpty()) {
|
||||
// 选中第一个结果并设置样式
|
||||
mlvContracts.setSelection(0);
|
||||
mlvContracts.setItemChecked(0, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +269,7 @@ public class ComposeSMSActivity extends BaseActivity {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 初始化或更新列表适配器
|
||||
// 初始化或更新列表适配器
|
||||
private void initAdapter(List<PhoneBean> initData) {
|
||||
mAdapterData.clear(); // 清空旧数据
|
||||
final PhoneUtil phoneUtil = new PhoneUtil(this);
|
||||
@@ -280,19 +303,45 @@ public class ComposeSMSActivity extends BaseActivity {
|
||||
mSimpleAdapter.setDropDownViewResource(R.layout.listview_contracts);
|
||||
mlvContracts.setAdapter(mSimpleAdapter);
|
||||
|
||||
// 列表项点击事件
|
||||
// 列表项点击事件:点击时主动设置选中状态,确保样式突显
|
||||
mlvContracts.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (position < mAdapterData.size()) {
|
||||
// 1. 主动设置当前项为选中状态
|
||||
mlvContracts.setItemChecked(position, true);
|
||||
// 2. 更新号码输入框和姓名显示
|
||||
String phone = mAdapterData.get(position).get(MAP_PHONE).toString();
|
||||
metTO.setText(phone);
|
||||
mtvTOName.setText(phoneUtil.getNameByPhone(phone));
|
||||
// 3. 滚动到点击位置(确保可见)
|
||||
mlvContracts.setSelection(position);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 列表项选中状态变化监听(可选,增强选中反馈)
|
||||
mlvContracts.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
// 选中时可添加额外反馈(如改变文本颜色,可选)
|
||||
if (view != null) {
|
||||
TextView tvName = (TextView) view.findViewById(R.id.listviewcontractsTextView1);
|
||||
TextView tvPhone = (TextView) view.findViewById(R.id.listviewcontractsTextView2);
|
||||
if (tvName != null) tvName.setTextColor(getResources().getColor(R.color.white));
|
||||
if (tvPhone != null) tvPhone.setTextColor(getResources().getColor(R.color.white));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
// 未选中时无操作
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mSimpleAdapter.notifyDataSetChanged(); // 数据更新时通知适配器
|
||||
// 数据更新时,先取消所有旧选中状态,再通知适配器刷新
|
||||
mlvContracts.clearChoices();
|
||||
mSimpleAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,19 +11,19 @@ import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.ContactsContract;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libapputils.utils.RegexPPiUtils;
|
||||
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;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
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;
|
||||
|
||||
public class PhoneUtil {
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package cc.winboll.studio.mymessagemanager.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@188.com>
|
||||
* @Date 2024/12/09 19:00:21
|
||||
* @Describe .* 前置预防针
|
||||
regex pointer preventive injection
|
||||
简称 RegexPPi
|
||||
*/
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class RegexPPiUtils {
|
||||
|
||||
public static final String TAG = "RegexPPiUtils";
|
||||
|
||||
//
|
||||
// 检验文本是否满足适合正则表达式模式计算
|
||||
//
|
||||
public static boolean isPPiOK(String text) {
|
||||
//String text = "这里是一些任意的文本内容";
|
||||
String regex = ".*";
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher matcher = pattern.matcher(text);
|
||||
/*if (matcher.matches()) {
|
||||
System.out.println("文本满足该正则表达式模式");
|
||||
} else {
|
||||
System.out.println("文本不满足该正则表达式模式");
|
||||
}*/
|
||||
return matcher.matches();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ package cc.winboll.studio.mymessagemanager.utils;
|
||||
import android.content.Context;
|
||||
import android.util.JsonReader;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libapputils.utils.RegexPPiUtils;
|
||||
import cc.winboll.studio.mymessagemanager.beans.SMSAcceptRuleBean;
|
||||
import cc.winboll.studio.mymessagemanager.beans.SMSAcceptRuleBean_V1;
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 选中状态:深灰色背景(可根据需求调整颜色) -->
|
||||
<item android:state_selected="true" android:drawable="@color/list_item_selected"/>
|
||||
<!-- 按压状态:浅灰色背景 -->
|
||||
<item android:state_pressed="true" android:drawable="@color/list_item_pressed"/>
|
||||
<!-- 默认状态:透明背景 -->
|
||||
<item android:drawable="@android:color/transparent"/>
|
||||
</selector>
|
||||
@@ -1,109 +1,112 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<cc.winboll.studio.libaes.views.AToolbar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/toolbar_height"
|
||||
android:id="@+id/activitycomposesmsASupportToolbar1"/>
|
||||
<cc.winboll.studio.libaes.views.AToolbar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/toolbar_height"
|
||||
android:id="@+id/activitycomposesmsASupportToolbar1"/>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_frame">
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_frame">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/activitycomposesmsRelativeLayout1">
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/activitycomposesmsRelativeLayout1">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/activitycomposesmsLinearLayout1"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_alignParentLeft="true">
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/activitycomposesmsLinearLayout1"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_alignParentLeft="true">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="(拼音搜索):"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<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"/>
|
||||
<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"/>
|
||||
<EditText
|
||||
android:layout_width="wrap_content"
|
||||
android:inputType="phone"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:id="@+id/activitycomposesmsEditText1"/>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:padding="10dp"
|
||||
android:layout_weight="1.0">
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:padding="10dp"
|
||||
android:layout_weight="1.0">
|
||||
|
||||
<ListView
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@+id/activitycomposesmsinclude1"
|
||||
android:id="@+id/activitycomposesmsListView1"/>
|
||||
<!-- 关键修改:添加 listSelector 属性,关联选中样式 -->
|
||||
<ListView
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@+id/activitycomposesmsinclude1"
|
||||
android:id="@+id/activitycomposesmsListView1"
|
||||
android:listSelector="@drawable/listview_item_selector"
|
||||
android:choiceMode="singleChoice"/> <!-- 开启单选模式,确保选中状态唯一 -->
|
||||
|
||||
<include
|
||||
layout="@layout/view_smssend"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/activitycomposesmsinclude1"/>
|
||||
<include
|
||||
layout="@layout/view_smssend"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/activitycomposesmsinclude1"/>
|
||||
|
||||
</RelativeLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
|
||||
<color name="colorSMSSendColor">#FFDCDA3D</color>
|
||||
<color name="colorSMSInboxColor">#FF3DDC84</color>
|
||||
<color name="colorTTSRuleViewBackgroundColor">#FFDCDA3D</color>
|
||||
@@ -24,4 +27,8 @@
|
||||
<color name="colorSMSInboxColorTao">#FFD9D9D9</color>
|
||||
<color name="colorTTSRuleViewBackgroundColorTao">#FFB4B4B4</color>
|
||||
|
||||
<!-- 列表项选中颜色(深灰) -->
|
||||
<color name="list_item_selected">#FF696969</color>
|
||||
<!-- 列表项按压颜色(浅灰) -->
|
||||
<color name="list_item_pressed">#FFE0E0E0</color>
|
||||
</resources>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sun Aug 31 06:21:48 CST 2025
|
||||
stageCount=12
|
||||
#Wed Sep 03 20:59:53 HKT 2025
|
||||
stageCount=13
|
||||
libraryProject=
|
||||
baseVersion=15.4
|
||||
publishVersion=15.4.11
|
||||
publishVersion=15.4.12
|
||||
buildCount=0
|
||||
baseBetaVersion=15.4.12
|
||||
baseBetaVersion=15.4.13
|
||||
|
||||
@@ -16,6 +16,7 @@ import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver;
|
||||
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
|
||||
import cc.winboll.studio.powerbell.utils.StringUtils;
|
||||
import java.util.ArrayList;
|
||||
import android.widget.Switch;
|
||||
|
||||
public class ClearRecordActivity extends Activity {
|
||||
|
||||
@@ -24,6 +25,7 @@ public class ClearRecordActivity extends Activity {
|
||||
AToolbar mAToolbar;
|
||||
TextView mtvRecordText;
|
||||
App mApplication;
|
||||
boolean mIsShowRecordWithEnter = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -79,7 +81,18 @@ public class ClearRecordActivity extends Activity {
|
||||
|
||||
void initRecordText() {
|
||||
ArrayList<BatteryInfoBean> listBatteryInfo = AppCacheUtils.getInstance(this).getArrayListBatteryInfo();
|
||||
String szRecordText = StringUtils.formatPCMListString(listBatteryInfo);
|
||||
mtvRecordText.setText(szRecordText);
|
||||
if (mIsShowRecordWithEnter) {
|
||||
String szRecordText = StringUtils.formatPCMListStringWithEnter(listBatteryInfo);
|
||||
mtvRecordText.setText(szRecordText);
|
||||
} else {
|
||||
String szRecordText = StringUtils.formatPCMListString(listBatteryInfo);
|
||||
mtvRecordText.setText(szRecordText);
|
||||
}
|
||||
}
|
||||
|
||||
public void onShowRecordWithEnter(View view) {
|
||||
Switch swShowRecordWithEnter = (Switch)view;
|
||||
mIsShowRecordWithEnter = swShowRecordWithEnter.isChecked();
|
||||
initRecordText();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,15 @@ public class StringUtils {
|
||||
return sz;
|
||||
}
|
||||
|
||||
public static String formatPCMListStringWithEnter(ArrayList<BatteryInfoBean> arrayListBatteryInfo) {
|
||||
String sz = "";
|
||||
for (int i = 0; i < arrayListBatteryInfo.size() - 1; i++) {
|
||||
//LogUtils.d(TAG, "arrayListBatteryInfo.get(i).getBattetyValue() is "+ Integer.toString(arrayListBatteryInfo.get(i).getBattetyValue()));
|
||||
sz = "\n" + arrayListBatteryInfo.get(i).getBattetyValue() + "%\n " + getTimespanDifference(arrayListBatteryInfo.get(i).getTimeStamp(), arrayListBatteryInfo.get(i + 1).getTimeStamp()) + " " + sz;
|
||||
}
|
||||
return sz;
|
||||
}
|
||||
|
||||
// 获取时间之间的时间跨度字符串。
|
||||
// Get timespan string between times.
|
||||
// 返回值: {(几天/)(几小时/)(几分钟/)(几秒钟)}
|
||||
|
||||
@@ -47,12 +47,32 @@
|
||||
android:layout_weight="1.0"
|
||||
android:background="@drawable/bg_frame">
|
||||
|
||||
<TextView
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Record Text"
|
||||
android:textStyle="bold"
|
||||
android:gravity="center_horizontal"/>
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Record Text"
|
||||
android:textStyle="bold"
|
||||
android:gravity="center_horizontal"
|
||||
android:layout_weight="1.0"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:background="#FFD5D5D5"/>
|
||||
|
||||
<Switch
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="<↲>"
|
||||
android:id="@+id/activityclearrecordSwitch1"
|
||||
android:onClick="onShowRecordWithEnter"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginRight="10dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -57,3 +57,11 @@
|
||||
// NumTable 项目编译设置
|
||||
//include ':numtable'
|
||||
//rootProject.name = "numtable"
|
||||
|
||||
// MidiPlayer 项目编译设置
|
||||
//include ':midiplayer'
|
||||
//rootProject.name = "midiplayer"
|
||||
|
||||
// WebPageSources 项目编译设置
|
||||
//include ':webpagesources'
|
||||
//rootProject.name = "webpagesources"
|
||||
@@ -1,5 +1,7 @@
|
||||
## TimpStamp
|
||||
## 时间戳工具集
|
||||
|
||||
#### 介绍
|
||||
时间戳工具集。常驻工具栏快捷拷贝一份时间戳到剪贴板的工具。
|
||||
|
||||
## 使用要点:
|
||||
1。常驻通知栏按钮的正常使用,
|
||||
|
||||
1
webpagesources/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
34
webpagesources/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# webpagesources
|
||||
|
||||
#### 介绍
|
||||
WinBoLl 网站专用网页浏览器,用于开发调试 WinBoLL 网站。
|
||||
|
||||
#### 软件架构
|
||||
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
|
||||
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
|
||||
|
||||
|
||||
#### Gradle 编译说明
|
||||
调试版编译命令 :gradle assembleBetaDebug
|
||||
阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh webpagesources
|
||||
|
||||
#### 使用说明
|
||||
|
||||
#### 参与贡献
|
||||
|
||||
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
webpagesources/app_update_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
76
webpagesources/build.gradle
Normal file
@@ -0,0 +1,76 @@
|
||||
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.webpagesources"
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 30
|
||||
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 'io.github.medyo:android-about-page:2.0.0'
|
||||
// 视图布局下拉控件
|
||||
api 'com.baoyz.pullrefreshlayout:library:1.2.0'
|
||||
// SSH
|
||||
api 'com.jcraft:jsch:0.1.55'
|
||||
// Html 解析
|
||||
api 'org.jsoup:jsoup:1.13.1'
|
||||
// 二维码类库
|
||||
api 'com.google.zxing:core:3.4.1'
|
||||
api 'com.journeyapps:zxing-android-embedded:3.6.0'
|
||||
// 应用介绍页类库
|
||||
api 'io.github.medyo:android-about-page:2.0.0'
|
||||
// 吐司类库
|
||||
api 'com.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
webpagesources/build.properties
Normal file
@@ -0,0 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Fri Jun 13 10:04:49 HKT 2025
|
||||
stageCount=7
|
||||
libraryProject=
|
||||
baseVersion=15.0
|
||||
publishVersion=15.0.6
|
||||
buildCount=0
|
||||
baseBetaVersion=15.0.7
|
||||
21
webpagesources/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
|
||||
25
webpagesources/src/beta/AndroidManifest.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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_winbollbeta">
|
||||
|
||||
<!-- Put flavor specific code here -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="cc.winboll.studio.webpagesources.beta.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_provider"/>
|
||||
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
6
webpagesources/src/beta/res/values/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="app_name">WebPageSources +</string>
|
||||
|
||||
</resources>
|
||||
109
webpagesources/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,109 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="cc.winboll.studio.webpagesources">
|
||||
|
||||
<!-- 拥有完全的网络访问权限 -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<!-- 读取您共享存储空间中的内容 -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- 修改或删除您共享存储空间中的内容 -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- 只能在前台获取精确的位置信息 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_winboll"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/WebPageSourceTheme"
|
||||
android:supportsRtl="true"
|
||||
android:resizeableActivity="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="standard"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="http"/>
|
||||
|
||||
<data android:scheme="https"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:mimeType="text/html"/>
|
||||
|
||||
<data android:mimeType="application/xhtml+xml"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="about"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="javascript"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activities.AboutActivity"
|
||||
android:label="AboutActivity"/>
|
||||
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="4.0"/>
|
||||
|
||||
<activity android:name=".GlobalApplication$CrashActivity"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,345 @@
|
||||
package cc.winboll.studio.webpagesources;
|
||||
|
||||
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,214 @@
|
||||
package cc.winboll.studio.webpagesources;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen@QQ.COM
|
||||
* @Date 2023/07/02 17:10:46
|
||||
* @Describe 主要启动窗口类
|
||||
*/
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.PersistableBundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.webkit.ValueCallback;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
|
||||
import cc.winboll.studio.webpagesources.R;
|
||||
import cc.winboll.studio.webpagesources.activities.AboutActivity;
|
||||
import cc.winboll.studio.webpagesources.fragment.SourcesFragment;
|
||||
import cc.winboll.studio.webpagesources.fragment.WebFragment;
|
||||
import cc.winboll.studio.webpagesources.view.StatusBarView;
|
||||
import com.hjq.toast.ToastUtils;
|
||||
|
||||
public class MainActivity extends AppCompatActivity implements IWinBoLLActivity {
|
||||
|
||||
public static final String TAG = "MainActivity";
|
||||
|
||||
public static final int REQUEST_ABOUT_ACTIVITY = 0;
|
||||
public static final int REQUEST_FILE_CHOOSER = 1;
|
||||
|
||||
public static final String MSG_UPDATE_URL = "MSG_UPDATE_URL";
|
||||
|
||||
static MainActivity _MainActivity;
|
||||
StatusBarView mStatusBarView;
|
||||
WebFragment mWebFragment;
|
||||
SourcesFragment mSourcesFragment;
|
||||
static boolean _mIsLoadedHomePage;
|
||||
|
||||
@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_main);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
FragmentTransaction ft = ((FragmentManager)getSupportFragmentManager()).beginTransaction();
|
||||
mSourcesFragment = new SourcesFragment();
|
||||
ft.add(R.id.activitymainFrameLayout1, mSourcesFragment, SourcesFragment.TAG);
|
||||
ft.hide(mSourcesFragment);
|
||||
mWebFragment = new WebFragment();
|
||||
//mWebFragment.setJSOnShowSourceListener(this);
|
||||
ft.add(R.id.activitymainFrameLayout1, mWebFragment, WebFragment.TAG);
|
||||
ft.show(mWebFragment);
|
||||
ft.commit();
|
||||
|
||||
mStatusBarView = findViewById(R.id.activitymainStatusBarView1);
|
||||
_MainActivity = this;
|
||||
postStatusBarMessage("主窗口加载完成。");
|
||||
//ToastUtils.show("Start");
|
||||
}
|
||||
|
||||
// @Override
|
||||
// protected void onNewIntent(Intent intent) {
|
||||
// super.onNewIntent(intent);
|
||||
// // 处理 onNewIntent 时的 Intent(Activity 已存在时调用)
|
||||
// handleIntent(intent);
|
||||
// }
|
||||
|
||||
private String getIntentUrl(Intent intent) {
|
||||
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
|
||||
//ToastUtils.show("ACTION_VIEW");
|
||||
Uri data = intent.getData();
|
||||
if (data != null) {
|
||||
String url = data.toString(); // 获取完整 URL
|
||||
String host = data.getHost(); // 获取主机名(如 "www.example.com")
|
||||
String path = data.getPath(); // 获取路径(如 "/page")
|
||||
|
||||
// 在界面显示 URL
|
||||
//tvUrl.setText("接收到的 URL:\n" + url);
|
||||
//ToastUtils.show(String.format("url %s", url));
|
||||
return url;
|
||||
|
||||
// 示例:打开系统浏览器访问该 URL
|
||||
// Intent browserIntent = new Intent(Intent.ACTION_VIEW, data);
|
||||
// startActivity(browserIntent);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void postStatusBarMessage(String msg) {
|
||||
if (_MainActivity != null) {
|
||||
_MainActivity.mStatusBarView.postMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
String intentUrl = getIntentUrl(getIntent());
|
||||
if (intentUrl != null && !intentUrl.trim().equals("")) {
|
||||
mWebFragment.loadUrl(intentUrl);
|
||||
} else {
|
||||
if (_mIsLoadedHomePage) {
|
||||
//ToastUtils.show("重新加载当前页面");
|
||||
mWebFragment.reloadLastUrl();
|
||||
} else {
|
||||
//ToastUtils.show("加载默认主页");
|
||||
mWebFragment.loadUrl(getApplicationContext().getString(R.string.app_homepage));
|
||||
_mIsLoadedHomePage = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.toolbar_main, menu);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
//return super.onOptionsItemSelected(item);
|
||||
switch (item.getItemId()) {
|
||||
case R.id.item_sources : {
|
||||
LogUtils.d(TAG, "item_web");
|
||||
FragmentTransaction ft = ((FragmentManager)getSupportFragmentManager()).beginTransaction();
|
||||
ft.hide(mWebFragment);
|
||||
ft.show(mSourcesFragment);
|
||||
ft.commit();
|
||||
break;
|
||||
}
|
||||
case R.id.item_web : {
|
||||
LogUtils.d(TAG, "item_sources");
|
||||
FragmentTransaction ft = ((FragmentManager)getSupportFragmentManager()).beginTransaction();
|
||||
ft.hide(mSourcesFragment);
|
||||
ft.show(mWebFragment);
|
||||
ft.commit();
|
||||
break;
|
||||
}
|
||||
case R.id.item_editor : {
|
||||
mSourcesFragment.shareHtml();
|
||||
break;
|
||||
}
|
||||
case R.id.item_log : {
|
||||
GlobalApplication.getWinBoLLActivityManager().startLogActivity(this);
|
||||
break;
|
||||
}
|
||||
case R.id.item_about : {
|
||||
GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(this, AboutActivity.class);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
//ToastUtils.show("onActivityResult Main");
|
||||
switch (requestCode) {
|
||||
case REQUEST_ABOUT_ACTIVITY : {
|
||||
LogUtils.d(TAG, "REQUEST_ABOUT_ACTIVITY");
|
||||
break;
|
||||
}
|
||||
case REQUEST_FILE_CHOOSER : {
|
||||
//ToastUtils.show("MainActivity.REQUEST_FILE_CHOOSER");
|
||||
ValueCallback<Uri[]> filePathCallback = mWebFragment.getWebView().getFilePathCallback();
|
||||
|
||||
if (filePathCallback == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Uri[] results = null;
|
||||
// 检查选择的文件是否为空
|
||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
String dataString = data.getDataString();
|
||||
if (dataString != null) {
|
||||
results = new Uri[]{Uri.parse(dataString)};
|
||||
}
|
||||
}
|
||||
|
||||
// 传递选择的文件给WebView
|
||||
filePathCallback.onReceiveValue(results);
|
||||
filePathCallback = null;
|
||||
|
||||
break;
|
||||
}
|
||||
default : {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package cc.winboll.studio.webpagesources.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen@QQ.COM
|
||||
* @Date 2024/07/14 13:20:33
|
||||
* @Describe AboutFragment Test
|
||||
*/
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
|
||||
import cc.winboll.studio.webpagesources.R;
|
||||
import com.hjq.toast.ToastUtils;
|
||||
|
||||
final public class AboutActivity extends AppCompatActivity implements IWinBoLLActivity {
|
||||
|
||||
public static final String TAG = "AboutActivity";
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_about);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.toolbar_about, menu);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.item_help) {
|
||||
ToastUtils.show("R.id.item_help");
|
||||
} else if (item.getItemId() == android.R.id.home) {
|
||||
GlobalApplication.getWinBoLLActivityManager().getInstance().finish(this);
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package cc.winboll.studio.webpagesources.common;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen@QQ.COM
|
||||
* @Date 2024/08/26 20:48:46
|
||||
* @Describe 登录验证对话框
|
||||
*/
|
||||
import android.content.DialogInterface;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.HttpAuthHandler;
|
||||
import android.webkit.WebView;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.PopupMenu;
|
||||
import androidx.cardview.widget.CardView;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.webpagesources.R;
|
||||
import cc.winboll.studio.webpagesources.models.AuthenticationBean;
|
||||
import cc.winboll.studio.webpagesources.common.AuthenticationUtils;
|
||||
import com.hjq.toast.ToastUtils;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class AuthLoginDialog {
|
||||
|
||||
public static final String TAG = "AuthLoginDialog";
|
||||
|
||||
WebView mWebView;
|
||||
static AuthenticationBean _mLastAuthenticationBean;
|
||||
EditText metUserName;
|
||||
EditText metPassword;
|
||||
AlertDialog mLoginAlertDialog;
|
||||
HttpAuthHandler mHttpAuthHandler;
|
||||
|
||||
public AuthLoginDialog(WebView view, HttpAuthHandler httpAuthHandler, String host, String realm) {
|
||||
mWebView = view;
|
||||
mHttpAuthHandler = httpAuthHandler;
|
||||
_mLastAuthenticationBean = new AuthenticationBean();
|
||||
_mLastAuthenticationBean.setHost(host);
|
||||
_mLastAuthenticationBean.setRealm(realm);
|
||||
}
|
||||
|
||||
public void show() {
|
||||
// 获取要登录的URL和已知的身份验证信息
|
||||
String authUrl = "[ " + _mLastAuthenticationBean.getHost() + " ] " + _mLastAuthenticationBean.getRealm();
|
||||
final AuthenticationUtils authenticationUtils = AuthenticationUtils.getInstance(mWebView.getContext());
|
||||
final ArrayList<AuthenticationBean> listInfo = authenticationUtils.getHostAuthenticationList(_mLastAuthenticationBean.getHost(), _mLastAuthenticationBean.getRealm());
|
||||
|
||||
final View llMain = mWebView.inflate(mWebView.getContext(), R.layout.dialog_login_auth, null);
|
||||
metUserName = llMain.findViewById(R.id.viewloginhttpEditText1);
|
||||
metPassword = llMain.findViewById(R.id.viewloginhttpEditText2);
|
||||
RecyclerView recyclerView = llMain.findViewById(R.id.dialogloginauthRecyclerView1);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(mWebView.getContext(), LinearLayoutManager.HORIZONTAL, false));
|
||||
AuthenticationBeanListAdapter adapter = new AuthenticationBeanListAdapter(listInfo);
|
||||
// 设置 RecyclerView 的适配器
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
if (listInfo.size() > 0) {
|
||||
metUserName.setText(listInfo.get(0).getUserName());
|
||||
metPassword.setText(listInfo.get(0).getPassword());
|
||||
LogUtils.d(TAG, "listInfo setText");
|
||||
}
|
||||
// 弹窗提示用户输入账号密码
|
||||
mLoginAlertDialog = new AlertDialog.Builder(mWebView.getContext())
|
||||
.setTitle("Login Required")
|
||||
.setMessage(authUrl + "\nPlease enter your credentials:")
|
||||
.setView(llMain)
|
||||
.setPositiveButton("OK", new DialogInterface.OnClickListener(){
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int p) {
|
||||
_mLastAuthenticationBean.setUserName(metUserName.getText().toString());
|
||||
_mLastAuthenticationBean.setPassword(metPassword.getText().toString());
|
||||
//LogUtils.d(TAG, "getUserName : " + _mLastAuthenticationBean.getUserName());
|
||||
//LogUtils.d(TAG, "getPassword : " + _mLastAuthenticationBean.getPassword());
|
||||
//LogUtils.d(TAG, "mHttpAuthHandler : " + mHttpAuthHandler);
|
||||
// 进入网站
|
||||
mHttpAuthHandler.proceed(_mLastAuthenticationBean.getUserName(), _mLastAuthenticationBean.getPassword());
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
public void checkAndSaveLastAuthenticationBean() {
|
||||
if (_mLastAuthenticationBean != null) {
|
||||
// 更新身份验证信息
|
||||
if (!_mLastAuthenticationBean.getHost().isEmpty()
|
||||
&& !_mLastAuthenticationBean.getRealm().isEmpty()
|
||||
&& !_mLastAuthenticationBean.getUserName().isEmpty()
|
||||
&& !_mLastAuthenticationBean.getPassword().isEmpty()) {
|
||||
final AuthenticationUtils authenticationUtils = AuthenticationUtils.getInstance(mWebView.getContext());
|
||||
authenticationUtils.saveAuthenticationInfo(_mLastAuthenticationBean);
|
||||
// 清理已保存,或者不需要保存的登录信息
|
||||
_mLastAuthenticationBean = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AuthenticationBeanListAdapter extends RecyclerView.Adapter {
|
||||
|
||||
ArrayList<AuthenticationBean> mDataList;
|
||||
public AuthenticationBeanListAdapter(ArrayList<AuthenticationBean> listInfo) {
|
||||
mDataList = listInfo;
|
||||
}
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mDataList.size();
|
||||
}
|
||||
|
||||
void deleteSMSRecycleItem(final int position) {
|
||||
YesNoAlertDialog.show(mWebView.getContext(),
|
||||
"密码删除提示",
|
||||
"请确认删除(" + mDataList.get(position).getUserName() + ")密码!"
|
||||
, (new YesNoAlertDialog.OnDialogResultListener(){
|
||||
|
||||
@Override
|
||||
public void onYes() {
|
||||
AuthenticationUtils authenticationUtils = AuthenticationUtils.getInstance(mWebView.getContext());
|
||||
authenticationUtils.deleteAuthenticationInfo(mDataList.get(position));
|
||||
mDataList.remove(position);
|
||||
notifyDataSetChanged();
|
||||
ToastUtils.show("密码已删除!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNo() {
|
||||
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
|
||||
final AuthenticationBean item = mDataList.get(position);
|
||||
if (holder.getItemViewType() == 0) {
|
||||
final SimpleViewHolder viewHolder = (SimpleViewHolder) holder;
|
||||
viewHolder.mtvUserName.setText(item.getUserName());
|
||||
viewHolder.mCardView.setOnClickListener(new View.OnClickListener(){
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
metUserName.setText(item.getUserName());
|
||||
metPassword.setText(item.getPassword());
|
||||
}
|
||||
});
|
||||
viewHolder.mCardView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View p1) {
|
||||
// 弹出复制菜单
|
||||
PopupMenu menu = new PopupMenu(mWebView.getContext(), viewHolder.mCardView);
|
||||
//加载菜单资源
|
||||
menu.getMenuInflater().inflate(R.menu.toolbar_authinfo, menu.getMenu());
|
||||
//设置点击事件的响应
|
||||
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem menuItem) {
|
||||
int nItemId = menuItem.getItemId();
|
||||
if (nItemId == R.id.item_delete_authinfo) {
|
||||
deleteSMSRecycleItem(position);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
//一定要调用show()来显示弹出式菜单
|
||||
menu.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
if (viewType == 0) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.listview_authinfo, parent, false);
|
||||
return new SimpleViewHolder(view);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
class SimpleViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView mtvUserName;
|
||||
CardView mCardView;
|
||||
SimpleViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
mtvUserName = itemView.findViewById(R.id.viewitemauthinfoTextView1);
|
||||
mCardView = itemView.findViewById(R.id.listviewauthinfoCardView1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package cc.winboll.studio.webpagesources.common;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen@QQ.COM
|
||||
* @Date 2024/08/26 16:16:00
|
||||
* @Describe 网站登录验证工具类
|
||||
*/
|
||||
import android.content.Context;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.webpagesources.models.AuthenticationBean;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class AuthenticationUtils {
|
||||
|
||||
public static final String TAG = "AuthenticationUtils";
|
||||
|
||||
static AuthenticationUtils _mAuthenticationUtils;
|
||||
static String _mBeanPath;
|
||||
Context mContext;
|
||||
ArrayList<AuthenticationBean> mData;
|
||||
|
||||
AuthenticationUtils(Context context) {
|
||||
mContext = context;
|
||||
File beanDir = new File(context.getFilesDir(), "home" + File.separator + TAG);
|
||||
if (!beanDir.exists()) {
|
||||
beanDir.mkdirs();
|
||||
}
|
||||
LogUtils.d(TAG, String.format("beanDir %s", beanDir.toString()));
|
||||
_mBeanPath = beanDir.getPath() + "/" + AuthenticationBean.class.getName() + ".json";
|
||||
mData = new ArrayList<AuthenticationBean>();
|
||||
AuthenticationBean.loadBeanListFromFile(_mBeanPath, mData, AuthenticationBean.class);
|
||||
}
|
||||
|
||||
public static AuthenticationUtils getInstance(Context context) {
|
||||
if (_mAuthenticationUtils == null) {
|
||||
_mAuthenticationUtils = new AuthenticationUtils(context);
|
||||
}
|
||||
return _mAuthenticationUtils;
|
||||
}
|
||||
|
||||
ArrayList<AuthenticationBean> loadAuthenticationData(String szHost, String szRealm) {
|
||||
ArrayList<AuthenticationBean> listReturn = new ArrayList<AuthenticationBean>();
|
||||
for (AuthenticationBean item : getInstance(mContext).mData) {
|
||||
if (szHost.equals(item.getHost())
|
||||
&& szRealm.equals(item.getRealm())) {
|
||||
listReturn.add(item);
|
||||
}
|
||||
}
|
||||
return listReturn;
|
||||
}
|
||||
|
||||
public ArrayList<AuthenticationBean> getHostAuthenticationList(String szHost, String szRealm) {
|
||||
ArrayList<AuthenticationBean> listReturn = new ArrayList<AuthenticationBean>();
|
||||
for (final AuthenticationBean item : mData) {
|
||||
if (szHost.equals(item.getHost())
|
||||
&& szRealm.equals(item.getRealm())) {
|
||||
listReturn.add(item);
|
||||
}
|
||||
}
|
||||
return listReturn;
|
||||
}
|
||||
|
||||
public void deleteAuthenticationInfo(AuthenticationBean bean) {
|
||||
for (final AuthenticationBean item : mData) {
|
||||
if (bean.getHost().equals(item.getHost())
|
||||
&& bean.getRealm().equals(item.getRealm())
|
||||
&& bean.getUserName().equals(item.getUserName())) {
|
||||
mData.remove(item);
|
||||
AuthenticationBean.saveBeanListToFile(_mBeanPath, mData);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void putAuthenticationBeanToTheFirstPosition(AuthenticationBean bean) {
|
||||
for (AuthenticationBean item : mData) {
|
||||
if (bean.getHost().equals(item.getHost())
|
||||
&& bean.getRealm().equals(item.getRealm())
|
||||
&& bean.getUserName().equals(item.getUserName())) {
|
||||
mData.add(0, new AuthenticationBean(item));
|
||||
mData.remove(item);
|
||||
AuthenticationBean.saveBeanListToFile(_mBeanPath, mData);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void saveAuthenticationInfo(AuthenticationBean bean) {
|
||||
saveAuthenticationInfo(bean.getHost(), bean.getRealm(), bean.getUserName(), bean.getPassword());
|
||||
}
|
||||
|
||||
public void saveAuthenticationInfo(String szHost, String szRealm, String szUserName, final String szPassword) {
|
||||
boolean isFindUserName = false;
|
||||
AuthenticationBean.loadBeanListFromFile(_mBeanPath, mData, AuthenticationBean.class);
|
||||
for (final AuthenticationBean item : mData) {
|
||||
if (szHost.equals(item.getHost())
|
||||
&& szRealm.equals(item.getRealm())
|
||||
&& szUserName.equals(item.getUserName())) {
|
||||
isFindUserName = true;
|
||||
if (!szPassword.equals(item.getPassword())) {
|
||||
// 如果找到同名信息,就提示是否更新
|
||||
YesNoAlertDialog.show(mContext, "Ask Update Password", "Is update " + item.getUserName() + " Password?", new YesNoAlertDialog.OnDialogResultListener(){
|
||||
@Override
|
||||
public void onNo() {
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onYes() {
|
||||
item.setPassword(szPassword);
|
||||
AuthenticationBean.saveBeanListToFile(_mBeanPath, mData);
|
||||
putAuthenticationBeanToTheFirstPosition(item);
|
||||
return;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
putAuthenticationBeanToTheFirstPosition(item);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找不到同名信息,就新建一个
|
||||
if (!isFindUserName) {
|
||||
AuthenticationBean bean = new AuthenticationBean(szHost, szRealm, szUserName, szPassword);
|
||||
mData.add(bean);
|
||||
AuthenticationBean.saveBeanListToFile(_mBeanPath, mData);
|
||||
putAuthenticationBeanToTheFirstPosition(bean);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package cc.winboll.studio.webpagesources.common;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen@QQ.COM
|
||||
* @Date 2023/06/22 15:01:43
|
||||
* @Describe 下载线程基类
|
||||
*/
|
||||
import android.content.Context;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
abstract public class BaseDownLoadThread implements Runnable {
|
||||
|
||||
public static final String TAG = "BaseDownLoadThread";
|
||||
|
||||
Context mContext;
|
||||
//UserLogonUtil mUserLogonUtil;
|
||||
String mszUrl;
|
||||
String mszDowndloadName;
|
||||
protected File mfDownloadDir;
|
||||
protected File mfDownloadFile;
|
||||
|
||||
//
|
||||
// 构造函数
|
||||
//
|
||||
public BaseDownLoadThread(Context context) {
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
// 构造函数
|
||||
//
|
||||
public BaseDownLoadThread(Context context, String szUrl) {
|
||||
mContext = context;
|
||||
File fDownloadDir = new File(context.getExternalCacheDir(), "Download");
|
||||
initDownLoadEnvironment(fDownloadDir, szUrl);
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// 构造函数
|
||||
//
|
||||
public BaseDownLoadThread(Context context, String szUrl, String szDowndloadName) {
|
||||
mContext = context;
|
||||
mszDowndloadName = szDowndloadName;
|
||||
File fDownloadDir = new File(context.getExternalCacheDir(), "Download");
|
||||
initDownLoadEnvironment(fDownloadDir, szUrl);
|
||||
}
|
||||
|
||||
//
|
||||
// 构造函数
|
||||
//
|
||||
public BaseDownLoadThread(File fDownloadDir, String szUrl) {
|
||||
initDownLoadEnvironment(fDownloadDir, szUrl);
|
||||
}
|
||||
|
||||
//
|
||||
// 初始化下载环境参数
|
||||
//
|
||||
void initDownLoadEnvironment(File fDownloadDir, String szUrl) {
|
||||
mfDownloadDir = fDownloadDir;
|
||||
//LogUtils.d(TAG, "mfDownloadDir is : " + mfDownloadDir);
|
||||
if (!mfDownloadDir.exists()) {
|
||||
mfDownloadDir.mkdir();
|
||||
}
|
||||
this.mszUrl = szUrl;
|
||||
//LogUtils.d(TAG, "mszUrl is : " + mszUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
//LogUtils.d(TAG, "DownLoad Start : " + mszUrl);
|
||||
InputStream in = null;
|
||||
FileOutputStream fout = null;
|
||||
try {
|
||||
//LogUtils.d(TAG, "mszUrl is " + mszUrl);
|
||||
URL httpUrl = new URL(mszUrl);
|
||||
HttpURLConnection conn = (HttpURLConnection) httpUrl.openConnection();
|
||||
//conn.setRequestProperty(UserLogonUtil.getInstance(mContext).getWinBollAppHeaderKey(),
|
||||
// UserLogonUtil.getInstance(mContext).getWinBollAppHeaderValue());
|
||||
conn.setDoInput(true);
|
||||
conn.setDoOutput(false);
|
||||
in = conn.getInputStream();
|
||||
|
||||
// 打印 Header Fields
|
||||
/*Map<String, List<String>> map = conn.getHeaderFields();
|
||||
for (String str : map.keySet()) {
|
||||
if (str != null) {
|
||||
LogUtils.d(TAG, str + map.get(str));
|
||||
}
|
||||
}*/
|
||||
|
||||
// 设置文件名
|
||||
//URL absUrl = conn.getURL();
|
||||
String filename = "";
|
||||
|
||||
// 文件名生成方法 1
|
||||
// 读取 Content-Disposition
|
||||
try {
|
||||
//LogUtils.d(TAG, "文件名生成方法 1");
|
||||
// 通过Content-Disposition获取文件名,这点跟服务器有关,需要灵活变通
|
||||
String szTemp = conn.getHeaderField("Content-Disposition");
|
||||
if (szTemp != null) {
|
||||
String szSearch = "filename=\"";
|
||||
int nSearchStart = szTemp.indexOf(szSearch);
|
||||
int nFileNameStart = nSearchStart + szSearch.length();
|
||||
//LogUtils.d(TAG, "nFileNameStart : " + Integer.toString(nFileNameStart));
|
||||
int nFileNameEnd = szTemp.indexOf("\"", nFileNameStart);
|
||||
//LogUtils.d(TAG, "nFileNameEnd : " + Integer.toString(nFileNameEnd));
|
||||
filename = szTemp.substring(nFileNameStart, nFileNameEnd);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
|
||||
}
|
||||
//LogUtils.d(TAG, "filename is : " + filename);
|
||||
|
||||
// 文件名生成方法 2
|
||||
// 读取 mszUrl
|
||||
// 如果文件名包含"/"字符
|
||||
// (例如 https://studio.zhangsken.cc/gitweb/libapputils.git/blob_plain/2aa898149aa7f0e34fe5bc11b2e70187f9f206bc:/libapputils/src/main/java/cc/winboll/studio/libapputils/LogView.java
|
||||
// 的文件名方法1就得到"filename is : libapputils/src/main/java/cc/winboll/studio/libapputils/LogView.java")
|
||||
// 或者为空就执行以下函数段
|
||||
if ((filename.lastIndexOf("/") > 0)
|
||||
|| filename.trim().equals("")) {
|
||||
//LogUtils.d(TAG, "文件名生成方法 2");
|
||||
// 示例 https://studio.zhangsken.cc/gitweb/libaes.git/rss
|
||||
// 截取最后的名称"rss"。
|
||||
// 示例 https://studio.zhangsken.cc/gitweb/libaes.git/
|
||||
// 最后是反斜杆结尾。就截取最后两个斜杆之间的名称"libaes.git"。
|
||||
Pattern pattern = Pattern.compile(".*[/]([^/]+)[/]?$", Pattern.MULTILINE);
|
||||
Matcher matcher = pattern.matcher(mszUrl);
|
||||
if (matcher.find()) {
|
||||
filename = matcher.replaceAll("$1");
|
||||
}
|
||||
|
||||
//LogUtils.d(TAG, "转化文件名为可操作易读写格式");
|
||||
//LogUtils.d(TAG, "filename is : " + filename);
|
||||
// 转化文件名为可操作易读写格式
|
||||
String filenameTemp = URLDecoder.decode(filename, "UTF-8");
|
||||
//filenameTemp = filenameTemp.replace("/", "_");
|
||||
//filename = filenameTemp.replace(".", "-");
|
||||
filename = filenameTemp;
|
||||
|
||||
// 如果文件名没有".*"后缀,就加上".dat"后缀
|
||||
if (filename.lastIndexOf(".") < 0) {
|
||||
filename += ".dat";
|
||||
}
|
||||
}
|
||||
//LogUtils.d(TAG, "filename is : " + filename);
|
||||
|
||||
// 如果指定了保存名称就用指定的名称
|
||||
if (mszDowndloadName != null && !mszDowndloadName.equals("")) {
|
||||
filename = mszDowndloadName;
|
||||
}
|
||||
|
||||
mfDownloadFile = createSavedFile(filename);
|
||||
// 如果文件存在就退出,没有就创建一个并下载
|
||||
if (mfDownloadFile.exists()) {
|
||||
LogUtils.d(TAG, "mfDownloadFile : " + mfDownloadFile.getPath());
|
||||
fout = new FileOutputStream(mfDownloadFile);
|
||||
byte[] buffer = new byte[1024];
|
||||
int len;
|
||||
while ((len = in.read(buffer)) != -1) {
|
||||
fout.write(buffer, 0, len);
|
||||
}
|
||||
LogUtils.i(TAG, "DownLoad is well done : " + mfDownloadFile.getPath());
|
||||
// 处理接收到的项目列表文件
|
||||
handleDownloadFile();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
|
||||
} finally {
|
||||
if (in != null) {
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
|
||||
}
|
||||
}
|
||||
if (fout != null) {
|
||||
try {
|
||||
fout.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
File createSavedFile(String szOriginDownloadName) {
|
||||
File fOriginDownload = new File(mfDownloadDir, szOriginDownloadName);
|
||||
if (!fOriginDownload.exists()) {
|
||||
// 如果该文件名的文件不存在,就表示可以保存为该文件名
|
||||
// 预先创建新文件
|
||||
try {
|
||||
fOriginDownload.createNewFile();
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
return fOriginDownload;
|
||||
}
|
||||
|
||||
// 校验文件是否已存在,若存在就重命名新文件名
|
||||
File fCheck = fOriginDownload;
|
||||
int nAddName = 1;
|
||||
while (fCheck.exists()) {
|
||||
int nPoint = szOriginDownloadName.lastIndexOf(".");
|
||||
if (nPoint > -1) {
|
||||
LogUtils.d(TAG, "nPoint > -1");
|
||||
fCheck = new File(mfDownloadDir, File.separator + szOriginDownloadName.substring(0, nPoint) + "(" + Integer.toString(nAddName) + ")." + szOriginDownloadName.substring(nPoint + 1));
|
||||
} else {
|
||||
LogUtils.d(TAG, "nPoint > -1 else");
|
||||
fCheck = new File(mfDownloadDir, File.separator + szOriginDownloadName + "(" + Integer.toString(nAddName) + ")");
|
||||
}
|
||||
LogUtils.d(TAG, "nAddName++");
|
||||
nAddName++;
|
||||
}
|
||||
// 预先创建新文件
|
||||
try {
|
||||
fCheck.createNewFile();
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
return fCheck;
|
||||
}
|
||||
|
||||
//
|
||||
// 文件下载后的文件处理函数
|
||||
//
|
||||
abstract protected void handleDownloadFile();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
package cc.winboll.studio.webpagesources.common;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.net.http.SslError;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.Gravity;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.DownloadListener;
|
||||
import android.webkit.HttpAuthHandler;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import android.webkit.SslErrorHandler;
|
||||
import android.webkit.ValueCallback;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebResourceError;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebResourceResponse;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.view.GestureDetectorCompat;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.webpagesources.MainActivity;
|
||||
import cc.winboll.studio.webpagesources.R;
|
||||
import cc.winboll.studio.webpagesources.thread.LinkDownLoadThread;
|
||||
import cc.winboll.studio.webpagesources.util.UIUtil;
|
||||
import cc.winboll.studio.webpagesources.view.ItemLongClickedPopWindow;
|
||||
import com.hjq.toast.ToastUtils;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class BaseWebView extends WebView {
|
||||
|
||||
public static final String TAG = "BaseWebView";
|
||||
|
||||
public static final int WEB_CopyLink = 0;
|
||||
public static final int WEB_DownLoadLink = 1;
|
||||
|
||||
protected Context mContext;
|
||||
static String _mszLastUrl = "";
|
||||
IOnPageFinished mIOnPageFinished;
|
||||
protected MyWebChromeClient mMyWebChromeClient;
|
||||
protected BaseWebViewClient mBaseWebViewClient;
|
||||
protected BaseWebViewHandler mBaseWebViewHandler;
|
||||
private int downX, downY;
|
||||
private GestureDetectorCompat mGestureDetector;
|
||||
ValueCallback<Uri[]> mFilePathCallback;
|
||||
WebChromeClient.FileChooserParams mFileChooserParams;
|
||||
|
||||
AuthLoginDialog mAuthLoginDialog;
|
||||
|
||||
public BaseWebView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public BaseWebView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mContext = context;
|
||||
|
||||
mBaseWebViewHandler = new BaseWebViewHandler();
|
||||
MyWebChromeClient mMyWebChromeClient = new MyWebChromeClient();
|
||||
setWebChromeClient(mMyWebChromeClient);
|
||||
mBaseWebViewClient = new BaseWebViewClient();
|
||||
setWebViewClient(mBaseWebViewClient);
|
||||
|
||||
WebSettings mWebSettings = getSettings();
|
||||
|
||||
// ------------------- 优化后的 WebSettings 配置 -------------------
|
||||
// 基础功能配置
|
||||
mWebSettings.setJavaScriptEnabled(true); // 启用JavaScript
|
||||
mWebSettings.setJavaScriptCanOpenWindowsAutomatically(true); // 允许JS打开新窗口
|
||||
mWebSettings.setDomStorageEnabled(true); // 启用DOM存储
|
||||
mWebSettings.setDatabaseEnabled(true); // 启用数据库存储
|
||||
//mWebSettings.setAppCacheEnabled(true); // 启用应用缓存
|
||||
String appCachePath = context.getCacheDir().getAbsolutePath();
|
||||
//mWebSettings.setAppCachePath(appCachePath);
|
||||
mWebSettings.setCacheMode(WebSettings.LOAD_DEFAULT); // 缓存模式
|
||||
|
||||
// 页面渲染与缩放
|
||||
mWebSettings.setUseWideViewPort(true); // 支持viewport标签
|
||||
mWebSettings.setLoadWithOverviewMode(true); // 自动加载时适应屏幕
|
||||
mWebSettings.setSupportZoom(true); // 支持缩放
|
||||
mWebSettings.setBuiltInZoomControls(true); // 显示缩放控件
|
||||
mWebSettings.setDisplayZoomControls(false); // 隐藏默认缩放控件
|
||||
mWebSettings.setDefaultTextEncodingName("utf-8"); // 默认编码
|
||||
mWebSettings.setMinimumFontSize(12); // 最小字体大小
|
||||
|
||||
// 性能优化
|
||||
mWebSettings.setLoadsImagesAutomatically(true); // 自动加载图片
|
||||
mWebSettings.setBlockNetworkImage(false); // 不阻止图片加载
|
||||
mWebSettings.setRenderPriority(WebSettings.RenderPriority.HIGH); // 高渲染优先级
|
||||
|
||||
// 混合内容配置(Android 5.0+)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
mWebSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
|
||||
}
|
||||
|
||||
// 安全配置
|
||||
mWebSettings.setSafeBrowsingEnabled(true); // 启用安全浏览
|
||||
mWebSettings.setAllowFileAccess(true); // 允许访问文件URL
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
mWebSettings.setAllowFileAccessFromFileURLs(false); // 禁止文件URL访问其他文件
|
||||
mWebSettings.setAllowUniversalAccessFromFileURLs(false); // 禁止文件URL执行JS
|
||||
}
|
||||
|
||||
// 高级功能(可选)
|
||||
mWebSettings.setGeolocationEnabled(true); // 启用地理位置
|
||||
mWebSettings.setNeedInitialFocus(true); // 获取初始焦点
|
||||
mWebSettings.setSaveFormData(true); // 保存表单数据
|
||||
mWebSettings.setEnableSmoothTransition(true); // 平滑过渡动画
|
||||
|
||||
// 硬件加速(Android 3.0+)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
setLayerType(View.LAYER_TYPE_HARDWARE, null);
|
||||
}
|
||||
|
||||
// 字体缩放(100%为默认)
|
||||
mWebSettings.setTextZoom(100);
|
||||
// ------------------- WebSettings 配置结束 -------------------
|
||||
|
||||
mWebSettings.setSafeBrowsingEnabled(true);
|
||||
addJavascriptInterface(new JSConsole(mContext), "console");
|
||||
addJavascriptInterface(new JS(mContext), "local_obj");
|
||||
|
||||
setInitialScale(60);
|
||||
|
||||
mGestureDetector = new GestureDetectorCompat(mContext, new GestureDetector.SimpleOnGestureListener() {
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
downX = (int) e.getX();
|
||||
downY = (int) e.getY();
|
||||
}
|
||||
});
|
||||
|
||||
setOnLongClickListener(new View.OnLongClickListener() {
|
||||
String szUrl = "";
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
|
||||
WebView.HitTestResult result = ((WebView)v).getHitTestResult();
|
||||
if (null == result)
|
||||
return false;
|
||||
int type = result.getType();
|
||||
if (type == WebView.HitTestResult.UNKNOWN_TYPE)
|
||||
return false;
|
||||
if (type == WebView.HitTestResult.EDIT_TEXT_TYPE) {
|
||||
}
|
||||
final ItemLongClickedPopWindow itemLongClickedPopWindow = new ItemLongClickedPopWindow(mContext, ItemLongClickedPopWindow.IMAGE_VIEW_POPUPWINDOW, UIUtil.dip2px(mContext, 180), ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
szUrl = result.getExtra();
|
||||
|
||||
itemLongClickedPopWindow.showAtLocation(v, Gravity.TOP | Gravity.LEFT, downX, downY + 10);
|
||||
|
||||
itemLongClickedPopWindow.getView(R.id.item_longclicked_copylink)
|
||||
.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("simple text", szUrl);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(mContext, "Copy to clipboard.", Toast.LENGTH_SHORT).show();
|
||||
itemLongClickedPopWindow.dismiss();
|
||||
}
|
||||
});
|
||||
itemLongClickedPopWindow.getView(R.id.item_longclicked_downloadlink)
|
||||
.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
itemLongClickedPopWindow.dismiss();
|
||||
new Thread(new LinkDownLoadThread(szUrl)).start();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public BaseWebView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public String getLastUrl() {
|
||||
return _mszLastUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadUrl(String url) {
|
||||
MainActivity.postStatusBarMessage(String.format("正在加载:%s", url));
|
||||
Pattern patternHttp = Pattern.compile("(?i)^http[s]{0,1}://", Pattern.CASE_INSENSITIVE);
|
||||
Matcher matcherHttp = patternHttp.matcher(url);
|
||||
if (matcherHttp.matches()) {
|
||||
stopLoading();
|
||||
loadUrl(url);
|
||||
return;
|
||||
} else {
|
||||
Pattern pattern = Pattern.compile("^[a-z]+:.*", Pattern.MULTILINE);
|
||||
Matcher matcher = pattern.matcher(url);
|
||||
if (matcher.find()) {
|
||||
super.loadUrl(url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setFilePathCallback(ValueCallback<Uri[]> mFilePathCallback) {
|
||||
this.mFilePathCallback = mFilePathCallback;
|
||||
}
|
||||
|
||||
public ValueCallback<Uri[]> getFilePathCallback() {
|
||||
return mFilePathCallback;
|
||||
}
|
||||
|
||||
public void setFileChooserParams(WebChromeClient.FileChooserParams mFileChooserParams) {
|
||||
this.mFileChooserParams = mFileChooserParams;
|
||||
}
|
||||
|
||||
public WebChromeClient.FileChooserParams getFileChooserParams() {
|
||||
return mFileChooserParams;
|
||||
}
|
||||
|
||||
private class MyWebChromeClient extends WebChromeClient {
|
||||
@Override
|
||||
public void onProgressChanged(WebView view, int newProgress) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
|
||||
setFilePathCallback(filePathCallback);
|
||||
setFileChooserParams(fileChooserParams);
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
((AppCompatActivity)mContext).startActivityForResult(intent, MainActivity.REQUEST_FILE_CHOOSER);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
boolean ret = mGestureDetector.onTouchEvent(event);
|
||||
LogUtils.d(TAG, String.format("onTouchEvent ret : %s", ret));
|
||||
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
class BaseWebViewHandler extends Handler {
|
||||
|
||||
public static final String TAG = "BaseWebViewHandler";
|
||||
|
||||
public static final int MSG_RELOAD_LOG = 0X100;
|
||||
public static final int MSG_SHOW_DATA = 0X123;
|
||||
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_SHOW_DATA:{
|
||||
loadDataWithBaseURL("", (String)msg.obj, "text/html", "UTF-8", "");
|
||||
break;
|
||||
}
|
||||
}
|
||||
super.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
final class InJavaScriptLocalObj {
|
||||
@JavascriptInterface
|
||||
public void getSource(String html) {
|
||||
}
|
||||
}
|
||||
|
||||
public class MyDownLoadListener implements DownloadListener {
|
||||
private Context mContext;
|
||||
|
||||
public MyDownLoadListener(Context context) {
|
||||
this.mContext = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
|
||||
Uri uri = Uri.parse(url);
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
|
||||
mContext.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
class BaseWebViewClient extends WebViewClient {
|
||||
|
||||
public static final String TAG = "BaseWebViewClient";
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
view.loadUrl("javascript:window.local_obj.showSource('<head>'+" +
|
||||
"document.getElementsByTagName('html')[0].innerHTML+'</head>');");
|
||||
super.onPageFinished(view, url);
|
||||
mIOnPageFinished.onPageFinished(url);
|
||||
LogUtils.d(TAG, "Page load finished : " + url);
|
||||
_mszLastUrl = url;
|
||||
MainActivity.postStatusBarMessage(String.format("加载完成:%s", url));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
|
||||
//SSL证书错误修复方法:
|
||||
//网站证书下载网站是https://www.ssleye.com/ssltool/certs_down.html
|
||||
//使用该网站访问要获取证书的主机
|
||||
//获取证书后导出证书到 res/raw/<标识名称>.cer
|
||||
//再在res/xml/network_security_config.xml配置网站证书设置。
|
||||
ToastUtils.show("SSL证书错误!");
|
||||
LogUtils.d(TAG, "SSL证书错误! onReceivedSslError 0\nerror : " + error.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
|
||||
int statusCode = errorResponse.getStatusCode();
|
||||
LogUtils.d(TAG, "onReceivedHttpError statusCode " + Integer.toString(statusCode));
|
||||
super.onReceivedHttpError(view, request, errorResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedError(WebView view, int errorCode,
|
||||
String description, String failingUrl) {
|
||||
super.onReceivedError(view, errorCode, description, failingUrl);
|
||||
LogUtils.d(TAG, "onReceivedError 1\nerrorCode : " + errorCode
|
||||
+ "\ndescription :" + description + "\nfailingUrl : " + failingUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
|
||||
super.onReceivedError(view, request, error);
|
||||
LogUtils.d(TAG, "onReceivedError 2\nUrl : " + request.getUrl()
|
||||
+ "\nErrorCode : " + error.getErrorCode()
|
||||
+ "\nDescription : " + error.getDescription());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedLoginRequest(WebView view, String realm, String account, String args) {
|
||||
super.onReceivedLoginRequest(view, realm, account, args);
|
||||
LogUtils.d(TAG, "onReceivedLoginRequest");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadResource(WebView view, String url) {
|
||||
super.onLoadResource(view, url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedHttpAuthRequest(WebView view,
|
||||
HttpAuthHandler handler, String host, String realm) {
|
||||
mAuthLoginDialog = new AuthLoginDialog(view, handler, host, realm);
|
||||
mAuthLoginDialog.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
||||
super.onPageStarted(view, url, favicon);
|
||||
LogUtils.d(TAG, "Page load started : " + url);
|
||||
_mszLastUrl = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
view.loadUrl(url);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
|
||||
// 修复Android 5.0点击无反应的问题
|
||||
view.loadUrl(request.getUrl().toString());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IOnPageFinished {
|
||||
void onPageFinished(String url);
|
||||
}
|
||||
|
||||
public void setOnPageFinished(IOnPageFinished iOnPageFinished) {
|
||||
mIOnPageFinished = iOnPageFinished;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package cc.winboll.studio.webpagesources.common;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen@QQ.COM
|
||||
* @Date 2023/07/13 13:54:13
|
||||
* @Describe 获取网页内容的接口类
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import cc.winboll.studio.webpagesources.fragment.SourcesFragment;
|
||||
|
||||
public final class JS {
|
||||
|
||||
public static final String TAG = "JS";
|
||||
|
||||
Context mContext;
|
||||
|
||||
public JS(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
@SuppressWarnings("unused")
|
||||
public void showSource(String html) {
|
||||
//LogUtils.d(TAG, "showSource");
|
||||
SourcesFragment.sendSourcesUpdateMessage(html);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package cc.winboll.studio.webpagesources.common;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen@QQ.COM
|
||||
* @Date 2024/08/23 02:15:03
|
||||
* @Describe Javascript Console Log 接收类。
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class JSConsole {
|
||||
|
||||
public static final String TAG = "JSConsole";
|
||||
|
||||
Context mContext;
|
||||
|
||||
public JSConsole(Context context) {
|
||||
//LogUtils.d(TAG, "JSConsole(Context context)");
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
@SuppressWarnings("unused")
|
||||
public void log(String tag, String message) {
|
||||
LogUtils.i(TAG, "console.log(...)\n" + tag + " : " + message);
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
@SuppressWarnings("unused")
|
||||
public void log(String message) {
|
||||
LogUtils.i(TAG, "console.log(...)\n" + message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package cc.winboll.studio.webpagesources.fragment;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen@QQ.COM
|
||||
* @Date 2023/07/04 13:45:57
|
||||
* @Describe 网页源码视图 Fragment
|
||||
*/
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Message;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.webpagesources.R;
|
||||
import cc.winboll.studio.webpagesources.handler.SourcesFragmentHandler;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
|
||||
public class SourcesFragment extends Fragment {
|
||||
|
||||
public static final String TAG = "SourcesFragment";
|
||||
|
||||
View mView;
|
||||
EditText mEditText;
|
||||
String mszHtmlPath;
|
||||
String mszHtmlFileName;
|
||||
static SourcesFragmentHandler _mSourcesFragmentHandler;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
//return super.onCreateView(inflater, container, savedInstanceState);
|
||||
mView = inflater.inflate(R.layout.fragment_sources, container, false);
|
||||
mEditText = mView.findViewById(R.id.fragmentsourcesEditText1);
|
||||
|
||||
_mSourcesFragmentHandler = new SourcesFragmentHandler(this);
|
||||
mszHtmlFileName = getString(R.string.app_name) + ".html";
|
||||
mszHtmlPath = getActivity().getExternalFilesDir(TAG) + "/" + mszHtmlFileName;
|
||||
|
||||
return mView;
|
||||
}
|
||||
|
||||
public void showCurrentWebPageHtml(String szHtml) {
|
||||
/*File fHtml = new File(App._fTempHtmlPath);
|
||||
StringBuffer sb = new StringBuffer();
|
||||
try {
|
||||
BufferedReader in = null;
|
||||
in = new BufferedReader(new InputStreamReader(new FileInputStream(fHtml), "UTF-8"));
|
||||
String line = "";
|
||||
while ((line = in.readLine()) != null) {
|
||||
sb.append(line);
|
||||
sb.append("\n");
|
||||
}
|
||||
mEditText.setText(sb.toString());
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, "IOException : " + e.getMessage());
|
||||
}*/
|
||||
mEditText.setText(szHtml);
|
||||
}
|
||||
|
||||
public void shareHtml() {
|
||||
saveHtml(mEditText.getText().toString());
|
||||
|
||||
Uri uri;
|
||||
File file = new File(mszHtmlPath);
|
||||
if (Build.VERSION.SDK_INT >= 24) {//android 7.0以上
|
||||
uri = FileProvider.getUriForFile(getActivity().getApplicationContext(), getActivity().getApplicationContext().getPackageName() + ".fileprovider", file);
|
||||
} else {
|
||||
uri = Uri.fromFile(file);
|
||||
}
|
||||
Intent shareIntent = new Intent("android.intent.action.VIEW");
|
||||
shareIntent.setDataAndType(uri, "text/html");
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
}
|
||||
startActivity(Intent.createChooser(shareIntent, mszHtmlFileName));
|
||||
}
|
||||
|
||||
public static void sendSourcesUpdateMessage(String szHtml) {
|
||||
if (_mSourcesFragmentHandler != null) {
|
||||
Message message = _mSourcesFragmentHandler.obtainMessage(SourcesFragmentHandler.MSG_UPDATE);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(SourcesFragmentHandler.BUNDLE_KEY_HTML, szHtml);
|
||||
message.setData(bundle);
|
||||
_mSourcesFragmentHandler.sendMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
void saveHtml(String szHtml) {
|
||||
try {
|
||||
File fTempHtml = new File(mszHtmlPath);
|
||||
if (fTempHtml.exists()) {
|
||||
fTempHtml.delete();
|
||||
}
|
||||
BufferedWriter out = null;
|
||||
out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fTempHtml, false), "UTF-8"));
|
||||
out.write(szHtml);
|
||||
out.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, "IOException : " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package cc.winboll.studio.webpagesources.fragment;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen@QQ.COM
|
||||
* @Date 2023/07/04 13:45:32
|
||||
* @Describe 网页浏览视图 Fragment
|
||||
*/
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.widget.LinearLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import cc.winboll.studio.libaes.views.AButton;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.webpagesources.R;
|
||||
import cc.winboll.studio.webpagesources.common.BaseWebView;
|
||||
import cc.winboll.studio.webpagesources.view.URLAddressView;
|
||||
import com.baoyz.widget.PullRefreshLayout;
|
||||
|
||||
public class WebFragment extends Fragment {
|
||||
|
||||
public static final String TAG = "WebFragment";
|
||||
|
||||
View mView;
|
||||
BaseWebView mBaseWebView;
|
||||
URLAddressView mURLAddressView;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
//return super.onCreateView(inflater, container, savedInstanceState);
|
||||
mView = inflater.inflate(R.layout.fragment_web, container, false);
|
||||
|
||||
mURLAddressView = mView.findViewById(R.id.fragmentwebURLAddressView1);
|
||||
mURLAddressView.initAnchorView((LinearLayout)mView.findViewById(R.id.fragmentwebLinearLayout1));
|
||||
|
||||
AButton btnGoback = mView.findViewById(R.id.fragmentwebAButton1);
|
||||
btnGoback.setOnClickListener(new AButton.OnClickListener(){
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
mBaseWebView.goBack();
|
||||
}
|
||||
});
|
||||
AButton btnGoForward = mView.findViewById(R.id.fragmentwebAButton2);
|
||||
btnGoForward.setOnClickListener(new AButton.OnClickListener(){
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mURLAddressView.getURLAddressText().equals(mBaseWebView.getUrl())) {
|
||||
mBaseWebView.goForward();
|
||||
} else {
|
||||
mBaseWebView.stopLoading();
|
||||
mBaseWebView.loadUrl(mURLAddressView.getURLAddressText());
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
AButton btnReload = mView.findViewById(R.id.fragmentwebAButton3);
|
||||
btnReload.setOnClickListener(new AButton.OnClickListener(){
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
mBaseWebView.stopLoading();
|
||||
mBaseWebView.clearCache(true);
|
||||
mBaseWebView.reload();
|
||||
}
|
||||
});
|
||||
AButton btnStopLoading = mView.findViewById(R.id.fragmentwebAButton4);
|
||||
btnStopLoading.setOnClickListener(new AButton.OnClickListener(){
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
mBaseWebView.stopLoading();
|
||||
}
|
||||
});
|
||||
|
||||
mBaseWebView = mView.findViewById(R.id.fragmentwebBaseWebView1);
|
||||
mBaseWebView.setOnPageFinished(new BaseWebView.IOnPageFinished(){
|
||||
@Override
|
||||
public void onPageFinished(String url) {
|
||||
mURLAddressView.setURLAddressText(url);
|
||||
}
|
||||
});
|
||||
|
||||
final PullRefreshLayout layout = mView.findViewById(R.id.fragmentwebPullRefreshLayout1);
|
||||
// listen refresh event
|
||||
layout.setOnRefreshListener(new PullRefreshLayout.OnRefreshListener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
// start refresh
|
||||
mBaseWebView.stopLoading();
|
||||
mBaseWebView.clearCache(true);
|
||||
mBaseWebView.reload();
|
||||
layout.setRefreshing(false);
|
||||
}
|
||||
});
|
||||
return mView;
|
||||
}
|
||||
|
||||
public BaseWebView getWebView() {
|
||||
return mBaseWebView;
|
||||
}
|
||||
|
||||
public void loadUrl(String szUrl) {
|
||||
if(mBaseWebView != null) {
|
||||
mBaseWebView.loadUrl(szUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public void reloadLastUrl() {
|
||||
mBaseWebView.stopLoading();
|
||||
mBaseWebView.loadUrl(mBaseWebView.getLastUrl());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (mBaseWebView != null) {
|
||||
mBaseWebView.destroy();
|
||||
|
||||
ViewParent parent = mBaseWebView.getParent();
|
||||
if (parent != null) {
|
||||
((ViewGroup) parent).removeView(mBaseWebView);
|
||||
}
|
||||
mBaseWebView.stopLoading();
|
||||
// 退出时调用此方法,移除绑定的服务,否则某些特定系统会报错
|
||||
mBaseWebView.getSettings().setJavaScriptEnabled(false);
|
||||
mBaseWebView.clearHistory();
|
||||
mBaseWebView.clearView();
|
||||
mBaseWebView.removeAllViews();
|
||||
mBaseWebView.destroy();
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cc.winboll.studio.webpagesources.handler;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen@QQ.COM
|
||||
* @Date 2023/07/13 15:43:48
|
||||
* @Describe 源码视图窗口事务处理类
|
||||
*/
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import cc.winboll.studio.webpagesources.fragment.SourcesFragment;
|
||||
|
||||
public class SourcesFragmentHandler extends Handler {
|
||||
|
||||
public static final String TAG = "SourcesFragmentHandler";
|
||||
|
||||
public static final int MSG_UPDATE = 0;
|
||||
public static final String BUNDLE_KEY_HTML = "BUNDLE_KEY_HTML";
|
||||
|
||||
SourcesFragment mSourcesFragment;
|
||||
public SourcesFragmentHandler(SourcesFragment fragment) {
|
||||
mSourcesFragment = fragment;
|
||||
}
|
||||
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_UPDATE:{
|
||||
Bundle bundle = msg.getData();
|
||||
String szHtml = (String)bundle.getSerializable(BUNDLE_KEY_HTML);
|
||||
mSourcesFragment.showCurrentWebPageHtml(szHtml);
|
||||
break;
|
||||
}
|
||||
}
|
||||
super.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package cc.winboll.studio.webpagesources.models;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen@QQ.COM
|
||||
* @Date 2024/08/26 15:43:22
|
||||
* @Describe 验证信息数据类
|
||||
*/
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
|
||||
public class AuthenticationBean extends BaseBean {
|
||||
|
||||
public static final String TAG = "AuthenticationBean";
|
||||
|
||||
// 主机名
|
||||
String host;
|
||||
// 认证区域
|
||||
String realm;
|
||||
// 用户名
|
||||
String userName;
|
||||
// 密码
|
||||
String password;
|
||||
|
||||
public AuthenticationBean(String host, String realm, String userName, String password) {
|
||||
this.host = host;
|
||||
this.realm = realm;
|
||||
this.userName = userName;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public AuthenticationBean(AuthenticationBean bean) {
|
||||
this.host = bean.getHost();
|
||||
this.realm = bean.getRealm();
|
||||
this.userName = bean.getUserName();
|
||||
this.password = bean.getPassword();
|
||||
}
|
||||
|
||||
public AuthenticationBean() {
|
||||
this.host = "";
|
||||
this.realm = "";
|
||||
this.userName = "";
|
||||
this.password = "";
|
||||
}
|
||||
|
||||
public void setRealm(String realm) {
|
||||
this.realm = realm;
|
||||
}
|
||||
|
||||
public String getRealm() {
|
||||
return realm;
|
||||
}
|
||||
|
||||
public void setHost(String host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return AuthenticationBean.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
AuthenticationBean bean = this;
|
||||
jsonWriter.name("host").value(bean.getHost());
|
||||
jsonWriter.name("realm").value(bean.getRealm());
|
||||
jsonWriter.name("userName").value(bean.getUserName());
|
||||
jsonWriter.name("password").value(bean.getPassword());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
|
||||
if (name.equals("host")) {
|
||||
setHost(jsonReader.nextString());
|
||||
} else if (name.equals("realm")) {
|
||||
setRealm(jsonReader.nextString());
|
||||
} else if (name.equals("userName")) {
|
||||
setUserName(jsonReader.nextString());
|
||||
} else if (name.equals("password")) {
|
||||
setPassword(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,25 @@
|
||||
package cc.winboll.studio.webpagesources.thread;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen@QQ.COM
|
||||
* @Date 2023/06/22 21:19:59
|
||||
* @Describe 链接下载进程类
|
||||
*/
|
||||
import android.os.Environment;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.webpagesources.common.BaseDownLoadThread;
|
||||
import java.io.File;
|
||||
|
||||
public class LinkDownLoadThread extends BaseDownLoadThread {
|
||||
|
||||
public static final String TAG = "LinkDownLoadThread";
|
||||
|
||||
public LinkDownLoadThread(String szUrl) {
|
||||
super(new File(Environment.getExternalStorageDirectory(), "Download"), szUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleDownloadFile() {
|
||||
LogUtils.d(TAG, "handleDownloadFile");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package cc.winboll.studio.webpagesources.util;
|
||||
|
||||
/*
|
||||
* 网站证书校验工具类
|
||||
* Power By :
|
||||
* ZhanGSKen@QQ.COM
|
||||
* https://blog.csdn.net/
|
||||
* https://blog.csdn.net/lsyz0021/article/details/54669914
|
||||
*
|
||||
*/
|
||||
import android.net.http.SslCertificate;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class SSLUtil {
|
||||
public static final String TAG = "SSLUtil";
|
||||
|
||||
/**
|
||||
* SSL证书错误,手动校验https证书
|
||||
*
|
||||
* @param cert https证书
|
||||
* @param sha256Str sha256值
|
||||
* @return true通过,false失败
|
||||
*/
|
||||
public static boolean isSSLCertOk(SslCertificate cert, String sha256Str) {
|
||||
byte[] SSLSHA256 = hexToBytes(sha256Str.toLowerCase());
|
||||
Bundle bundle = SslCertificate.saveState(cert);
|
||||
if (bundle != null) {
|
||||
byte[] bytes = bundle.getByteArray("x509-certificate");
|
||||
if (bytes != null) {
|
||||
try {
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
Certificate ca = cf.generateCertificate(new ByteArrayInputStream(bytes));
|
||||
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
||||
byte[] key = sha256.digest(((X509Certificate) ca).getEncoded());
|
||||
return Arrays.equals(key, SSLSHA256);
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Exception : " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* hexString转byteArr
|
||||
* <p>例如:</p>
|
||||
* hexString2Bytes("00A8") returns { 0, (byte) 0xA8 }
|
||||
*
|
||||
* @param hexString
|
||||
* @return 字节数组
|
||||
*/
|
||||
public static byte[] hexToBytes(String hexString) {
|
||||
|
||||
if (hexString == null || hexString.trim().length() == 0)
|
||||
return null;
|
||||
|
||||
int length = hexString.length() / 2;
|
||||
char[] hexChars = hexString.toCharArray();
|
||||
byte[] bytes = new byte[length];
|
||||
String hexDigits = "0123456789abcdef";
|
||||
for (int i = 0; i < length; i++) {
|
||||
int pos = i * 2; // 两个字符对应一个byte
|
||||
int h = hexDigits.indexOf(hexChars[pos]) << 4; // 注1
|
||||
int l = hexDigits.indexOf(hexChars[pos + 1]); // 注2
|
||||
if (h == -1 || l == -1) { // 非16进制字符
|
||||
return null;
|
||||
}
|
||||
bytes[i] = (byte) (h | l);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package cc.winboll.studio.webpagesources.util;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen@QQ.COM
|
||||
* @Date 2023/06/23 03:57:30
|
||||
* @Describe 视图辅助工具类
|
||||
*/
|
||||
import android.content.Context;
|
||||
|
||||
public class UIUtil {
|
||||
|
||||
public static final String TAG = "UIUtil";
|
||||
|
||||
public static int dip2px(Context context, float dpValue) {
|
||||
float scale = context.getResources().getDisplayMetrics().density;
|
||||
return (int) (dpValue * scale + 0.5f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package cc.winboll.studio.webpagesources.view;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen@QQ.COM
|
||||
* @Date 2023/06/23 03:45:50
|
||||
* @Describe 网页浏览长按弹出菜单类
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.PopupWindow;
|
||||
import cc.winboll.studio.webpagesources.R;
|
||||
|
||||
public class ItemLongClickedPopWindow extends PopupWindow {
|
||||
/**
|
||||
* 书签条目弹出菜单 * @value * {@value} *
|
||||
*/
|
||||
public static final int FAVORITES_ITEM_POPUPWINDOW = 0;
|
||||
/**
|
||||
* 书签页面弹出菜单 * @value * {@value} *
|
||||
*/
|
||||
public static final int FAVORITES_VIEW_POPUPWINDOW = 1;
|
||||
/**
|
||||
* 历史条目弹出菜单 * @value * {@value} *
|
||||
*/
|
||||
public static final int HISTORY_ITEM_POPUPWINDOW = 3;
|
||||
/**
|
||||
* 历史页面弹出菜单 * @value * {@value} *
|
||||
*/
|
||||
public static final int HISTORY_VIEW_POPUPWINDOW = 4;
|
||||
/**
|
||||
* 图片项目弹出菜单 * @value * {@value} *
|
||||
*/
|
||||
public static final int IMAGE_VIEW_POPUPWINDOW = 5;
|
||||
/**
|
||||
* 超链接项目弹出菜单 * @value * {@value} *
|
||||
*/
|
||||
public static final int ACHOR_VIEW_POPUPWINDOW = 6;
|
||||
private LayoutInflater itemLongClickedPopWindowInflater;
|
||||
private View itemLongClickedPopWindowView;
|
||||
private Context context;
|
||||
private int type;
|
||||
|
||||
/**
|
||||
* 构造函数 * @param context 上下文 * @param width 宽度 * @param height 高度 *
|
||||
*/
|
||||
public ItemLongClickedPopWindow(Context context, int type, int width, int height) {
|
||||
super(context);
|
||||
this.context = context;
|
||||
this.type = type;
|
||||
//创建
|
||||
this.initTab();
|
||||
//设置默认选项
|
||||
setWidth(width);
|
||||
setHeight(height);
|
||||
setContentView(this.itemLongClickedPopWindowView);
|
||||
setOutsideTouchable(true);
|
||||
setFocusable(true);
|
||||
}
|
||||
|
||||
//实例化
|
||||
private void initTab() {
|
||||
this.itemLongClickedPopWindowInflater = LayoutInflater.from(this.context);
|
||||
switch (type) {
|
||||
// case FAVORITES_ITEM_POPUPWINDOW:
|
||||
// this.itemLongClickedPopWindowView = this.itemLongClickedPopWindowInflater.inflate(R.layout.list_item_longclicked_favorites, null);
|
||||
// break;
|
||||
// case FAVORITES_VIEW_POPUPWINDOW: //对于书签内容弹出菜单,未作处理
|
||||
// break;
|
||||
// case HISTORY_ITEM_POPUPWINDOW:
|
||||
// this.itemLongClickedPopWindowView = this.itemLongClickedPopWindowInflater.inflate(R.layout.list_item_longclicked_history, null);
|
||||
// break;
|
||||
// case HISTORY_VIEW_POPUPWINDOW: //对于历史内容弹出菜单,未作处理
|
||||
// break;
|
||||
// case ACHOR_VIEW_POPUPWINDOW: //超链接
|
||||
// break;
|
||||
case IMAGE_VIEW_POPUPWINDOW: //图片
|
||||
this.itemLongClickedPopWindowView = this.itemLongClickedPopWindowInflater.inflate(R.layout.list_item_longclicked_img, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public View getView(int id) {
|
||||
return this.itemLongClickedPopWindowView.findViewById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package cc.winboll.studio.webpagesources.view;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import cc.winboll.studio.webpagesources.R;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@188.com>
|
||||
* @Date 2025/06/11 04:11
|
||||
* @Describe 网页状态信息栏
|
||||
*/
|
||||
public class StatusBarView extends LinearLayout {
|
||||
|
||||
public static final String TAG = "StatusBar";
|
||||
|
||||
public static final int MESSAGE_NEWS = 0;
|
||||
|
||||
Context mContext;
|
||||
TextView mtvMessage;
|
||||
|
||||
public StatusBarView(Context context) {
|
||||
super(context);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public StatusBarView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public StatusBarView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public StatusBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
void initView(Context context) {
|
||||
mContext = context;
|
||||
View viewMain = inflate(context, R.layout.view_statusbar, null);
|
||||
mtvMessage = viewMain.findViewById(R.id.tv_message);
|
||||
|
||||
// 设置布局参数为MATCH_PARENT,并确保最大尺寸显示
|
||||
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
addView(viewMain, params);
|
||||
}
|
||||
|
||||
public void postMessage(String msg) {
|
||||
if (mContext != null) {
|
||||
Message message = new Message();
|
||||
message.what = MESSAGE_NEWS;
|
||||
message.obj = msg;
|
||||
mHandler.dispatchMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
Handler mHandler = new Handler(){
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
if (msg.what == MESSAGE_NEWS) {
|
||||
mtvMessage.setText((String)msg.obj);
|
||||
}
|
||||
//super.handleMessage(msg);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
package cc.winboll.studio.webpagesources.view;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen@QQ.COM
|
||||
* @Date 2023/07/16 17:52:43
|
||||
* @Describe 网页地址栏
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupWindow;
|
||||
import androidx.appcompat.widget.ListPopupWindow;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.webpagesources.R;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class URLAddressView extends LinearLayout {
|
||||
|
||||
public static final String TAG = "URLEditText";
|
||||
|
||||
Context mContext;
|
||||
EditText mURLAddressEditText;
|
||||
ImageView mRightImageView;
|
||||
ImageView mLeftImageView;
|
||||
ListPopupWindow mListPopupWindow;
|
||||
boolean mIsListPopupWindow_OnShow = false;
|
||||
long mnRightImageViewTouchCount = 0;
|
||||
String mszConfigPath;
|
||||
ArrayList<URLInfo> mlistURLInfo;
|
||||
//Drawable drawable_r;
|
||||
//Drawable drawable_l_no;
|
||||
//Drawable drawable_l_yes;
|
||||
LinearLayout mllAnchorView;
|
||||
|
||||
/**
|
||||
* 在java代码里new的时候会用到
|
||||
* @param context
|
||||
*/
|
||||
public URLAddressView(Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在xml布局文件中使用时自动调用
|
||||
* @param context
|
||||
*/
|
||||
public URLAddressView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 不会自动调用,如果有默认style时,在第二个构造函数中调用
|
||||
* @param context
|
||||
* @param attrs
|
||||
* @param defStyleAttr
|
||||
*/
|
||||
public URLAddressView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 只有在API版本>21时才会用到
|
||||
* 不会自动调用,如果有默认style时,在第二个构造函数中调用
|
||||
* @param context
|
||||
* @param attrs
|
||||
* @param defStyleAttr
|
||||
* @param defStyleRes
|
||||
*/
|
||||
//@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public URLAddressView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public void initAnchorView(LinearLayout llAnchorView) {
|
||||
mllAnchorView = llAnchorView;
|
||||
}
|
||||
|
||||
void init(Context context) {
|
||||
mContext = context;
|
||||
View viewMain = inflate(mContext, R.layout.view_urladdress, null);
|
||||
mURLAddressEditText = viewMain.findViewById(R.id.viewurladdressEditText1);
|
||||
mLeftImageView = viewMain.findViewById(R.id.viewurladdressImageView1);
|
||||
mRightImageView = viewMain.findViewById(R.id.viewurladdressImageView2);
|
||||
addView(viewMain);
|
||||
|
||||
mszConfigPath = context.getExternalFilesDir(TAG) + File.separator + "_mszConfigPath.json";
|
||||
mlistURLInfo = new ArrayList<URLInfo>();
|
||||
mlistURLInfo = loadListURLInfo();
|
||||
|
||||
mURLAddressEditText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
// 当文本发生改变时,这个方法会被调用
|
||||
//LogUtils.d(TAG, "Text changed to: " + s.toString());
|
||||
for (int i = 0; i < 0; i++) {
|
||||
if (mURLAddressEditText.equals(mlistURLInfo.get(i).getUrl())) {
|
||||
updateFavoriteButtonStatus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
updateFavoriteButtonStatus();
|
||||
/*if (mIOnTextChanged != null) {
|
||||
if (text != null) {
|
||||
mIOnTextChanged.onTextChanged(text.toString());
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {}
|
||||
});
|
||||
|
||||
mURLAddressEditText.setOnTouchListener(new View.OnTouchListener(){
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View view, MotionEvent motionEvent) {
|
||||
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
//ToastUtils.show("mURLAddressEditText ACTION_DOWN");
|
||||
if(!mIsListPopupWindow_OnShow) {
|
||||
mnRightImageViewTouchCount = 0;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
mLeftImageView.setOnTouchListener(new View.OnTouchListener(){
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View view, MotionEvent motionEvent) {
|
||||
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
changeURLFavoriteStatus(mURLAddressEditText.getText().toString());
|
||||
} else {
|
||||
LogUtils.d(TAG, "MotionEvent : " + Integer.toString(motionEvent.getAction()));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
mRightImageView.setOnTouchListener(new View.OnTouchListener(){
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View view, MotionEvent motionEvent) {
|
||||
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
if (mnRightImageViewTouchCount % 2 == 0) {
|
||||
// 弹出网址列表
|
||||
showListPopulWindow();
|
||||
}
|
||||
mnRightImageViewTouchCount++;
|
||||
} else {
|
||||
LogUtils.d(TAG, "MotionEvent : " + Integer.toString(motionEvent.getAction()));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
/*TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.test);
|
||||
String text = ta.getString(R.styleable.test_text);
|
||||
setText(text + " ZhanGSKen.CN");
|
||||
ta.recycle();*/
|
||||
//drawable_r = getResources().getDrawable(R.drawable.ic_form_dropdown);
|
||||
//drawable_n.setBounds(0, 0, drawable_n.getMinimumWidth(),drawable_n.getMinimumHeight()); //此为必须写的
|
||||
//drawable_r.setBounds(0, 0, 80, 80); //此为必须写的
|
||||
|
||||
//drawable_l_no = getResources().getDrawable(R.drawable.ic_favorite_outline);
|
||||
//drawable_n.setBounds(0, 0, drawable_n.getMinimumWidth(),drawable_n.getMinimumHeight()); //此为必须写的
|
||||
//drawable_l_no.setBounds(0, 0, 80, 80); //此为必须写的
|
||||
|
||||
//drawable_l_yes = getResources().getDrawable(R.drawable.ic_favorite);
|
||||
//drawable_n.setBounds(0, 0, drawable_n.getMinimumWidth(),drawable_n.getMinimumHeight()); //此为必须写的
|
||||
//drawable_l_yes.setBounds(0, 0, 80, 80); //此为必须写的
|
||||
|
||||
//setFavoriteNo();
|
||||
//setOnTouchListener(mOnTouchListener);
|
||||
//setMinWidth(100);
|
||||
//setPadding(10,0,10,0);
|
||||
//setPaddingRelative(0,0,0,0);
|
||||
|
||||
}
|
||||
|
||||
public String getURLAddressText() {
|
||||
return mURLAddressEditText.getText().toString();
|
||||
}
|
||||
|
||||
public void setURLAddressText(String szUrl) {
|
||||
mURLAddressEditText.setText(szUrl);
|
||||
}
|
||||
|
||||
//
|
||||
// 更新收藏按钮显示状态
|
||||
//
|
||||
void updateFavoriteButtonStatus() {
|
||||
if (isFavoritedURL(mURLAddressEditText.getText().toString())) {
|
||||
mLeftImageView.setBackgroundResource(R.drawable.ic_favorite);
|
||||
//setCompoundDrawables(drawable_l_yes, null, drawable_r, null);
|
||||
} else {
|
||||
mLeftImageView.setBackgroundResource(R.drawable.ic_favorite_outline);
|
||||
//setCompoundDrawables(drawable_l_no, null, drawable_r, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*@Override
|
||||
public void setOnTouchListener(View.OnTouchListener l) {
|
||||
super.setOnTouchListener(l);
|
||||
}*/
|
||||
|
||||
/*View.OnTouchListener mOnTouchListener = new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View view, MotionEvent event) {
|
||||
final int DRAWABLE_LEFT = 0;
|
||||
//final int DRAWABLE_TOP = 1;
|
||||
final int DRAWABLE_RIGHT = 2;
|
||||
//final int DRAWABLE_BOTTOM = 3;
|
||||
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||
if (event.getX() >= (getWidth() - getCompoundDrawables()[DRAWABLE_RIGHT].getBounds().width())) {
|
||||
// 弹出网址列表
|
||||
LogUtils.d(TAG, "mOnTouchListener 1");
|
||||
showListPopulWindow();
|
||||
return false;
|
||||
} else if (event.getX() <= (getCompoundDrawables()[DRAWABLE_LEFT].getBounds().width())) {
|
||||
// 修改输入框网址收藏状态
|
||||
LogUtils.d(TAG, "mOnTouchListener 2");
|
||||
changeURLFavoriteStatus(getText().toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};*/
|
||||
|
||||
//
|
||||
// 改变网址的收藏状态:
|
||||
// @返回值:收藏状态
|
||||
// 如果没收藏就设置收藏,收藏状态返回true;
|
||||
// 如果收藏了就取消收藏,收藏状态返回false;
|
||||
//
|
||||
public boolean changeURLFavoriteStatus(String szUrl) {
|
||||
if (szUrl == null || szUrl.equals("")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (URLInfo item : mlistURLInfo) {
|
||||
if (item.getUrl().equals(szUrl)) {
|
||||
mlistURLInfo.remove(item);
|
||||
saveListURLInfo();
|
||||
updateFavoriteButtonStatus();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
URLInfo newItem = new URLInfo(szUrl);
|
||||
mlistURLInfo.add(newItem);
|
||||
saveListURLInfo();
|
||||
updateFavoriteButtonStatus();
|
||||
return true;
|
||||
}
|
||||
|
||||
//
|
||||
// 读取网址的收藏状态
|
||||
//
|
||||
public boolean isFavoritedURL(String szUrl) {
|
||||
if ((mlistURLInfo == null) || (szUrl == null) || szUrl.equals("")) {return false;}
|
||||
|
||||
for (URLInfo item : mlistURLInfo) {
|
||||
if (item.getUrl().equals(szUrl)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//
|
||||
// 前置网址在收藏列表里的位置
|
||||
//
|
||||
public boolean postposesURL(String szUrl) {
|
||||
for (URLInfo item : mlistURLInfo) {
|
||||
if (item.getUrl().equals(szUrl)) {
|
||||
mlistURLInfo.add(0, new URLInfo(szUrl));
|
||||
mlistURLInfo.remove(item);
|
||||
saveListURLInfo();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//
|
||||
// 加载收藏的网址列表
|
||||
//
|
||||
public ArrayList<URLInfo> loadListURLInfo() {
|
||||
File fJson = new File(mszConfigPath);
|
||||
try {
|
||||
ArrayList<URLInfo> listTemp = readJsonStream(new FileInputStream(fJson));
|
||||
if (listTemp != null && listTemp.size() > 0) {
|
||||
mlistURLInfo.clear();
|
||||
mlistURLInfo.addAll(listTemp);
|
||||
} else {
|
||||
mlistURLInfo.clear();
|
||||
saveListURLInfo();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, "IOException : " + e.getMessage());
|
||||
mlistURLInfo = new ArrayList<URLInfo>();
|
||||
saveListURLInfo();
|
||||
}
|
||||
return mlistURLInfo;
|
||||
}
|
||||
|
||||
//
|
||||
// 读取 Json 文件
|
||||
//
|
||||
ArrayList<URLInfo> readJsonStream(InputStream in) throws IOException {
|
||||
JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
|
||||
return readURLInfoArrayList(reader);
|
||||
}
|
||||
|
||||
//
|
||||
// 读取 Json 文件的每一 Json 项
|
||||
//
|
||||
ArrayList<URLInfo> readURLInfoArrayList(JsonReader reader) throws IOException {
|
||||
ArrayList<URLInfo> list = new ArrayList<URLInfo>();
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
list.add(readURLInfo(reader));
|
||||
}
|
||||
reader.endArray();
|
||||
return list;
|
||||
}
|
||||
|
||||
//
|
||||
// 读取 Json 文件的某一 Json 项
|
||||
//
|
||||
URLInfo readURLInfo(JsonReader reader) throws IOException {
|
||||
URLInfo urlInfo = new URLInfo();
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
String name = reader.nextName();
|
||||
if (name.equals("url")) {
|
||||
urlInfo.setUrl(reader.nextString());
|
||||
} else {
|
||||
reader.skipValue();
|
||||
}
|
||||
}
|
||||
reader.endObject();
|
||||
return urlInfo;
|
||||
}
|
||||
|
||||
//
|
||||
// 写入 Json 文件的某一 Json 项
|
||||
//
|
||||
static void writeConfigs(JsonWriter writer, URLInfo urlInfo) throws IOException {
|
||||
writer.beginObject();
|
||||
writer.name("url").value(urlInfo.getUrl());
|
||||
writer.endObject();
|
||||
}
|
||||
|
||||
//
|
||||
// 保存收藏的网址列表
|
||||
//
|
||||
void saveListURLInfo() {
|
||||
try {
|
||||
File fJson = new File(mszConfigPath);
|
||||
writeJsonStream(new FileOutputStream(fJson, false), mlistURLInfo);
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, "IOException : " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 写入 Json 文件
|
||||
//
|
||||
void writeJsonStream(OutputStream out, ArrayList<URLInfo> listURLInfo) throws IOException {
|
||||
JsonWriter writer = new JsonWriter(new OutputStreamWriter(out, "UTF-8"));
|
||||
writer.setIndent(" ");
|
||||
writeConfigsArray(writer, listURLInfo);
|
||||
writer.close();
|
||||
}
|
||||
|
||||
//
|
||||
// 记录 Json 文件的某一 Json 项
|
||||
//
|
||||
void writeConfigsArray(JsonWriter writer, ArrayList<URLInfo> listURLInfo) throws IOException {
|
||||
writer.beginArray();
|
||||
for (URLInfo urlInfo : listURLInfo) {
|
||||
writeConfigs(writer, urlInfo);
|
||||
}
|
||||
writer.endArray();
|
||||
}
|
||||
|
||||
public class URLInfo {
|
||||
String url;
|
||||
|
||||
public URLInfo() {}
|
||||
public URLInfo(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 弹出网址下拉框
|
||||
// @ llAnchorView : 设置下拉列表基准控件
|
||||
//
|
||||
void showListPopulWindow() {
|
||||
//final String[] list = {"asg","hshsh"};
|
||||
int nListSize = mlistURLInfo.size();
|
||||
final String[] list = new String[nListSize];
|
||||
for (int i = 0; i < nListSize; i++) {
|
||||
list[i] = mlistURLInfo.get(i).getUrl();
|
||||
}
|
||||
//final String[] list = (String[])mSetStringFavorite.toArray(new String[0]);
|
||||
|
||||
|
||||
mListPopupWindow = new ListPopupWindow(mContext);
|
||||
mListPopupWindow.setWidth(LayoutParams.WRAP_CONTENT);
|
||||
mListPopupWindow.setHeight(LayoutParams.WRAP_CONTENT);
|
||||
//listPopupWindow.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, list));//用android内置布局,或设计自己的样式
|
||||
mListPopupWindow.setAdapter(new ArrayAdapter<String>(mContext, R.layout.view_popurllist, R.id.viewpopurllistTextView1, list));//用android内置布局,或设计自己的样式
|
||||
//设置下拉列表基准控件
|
||||
//LinearLayout llAnchorView = findViewById(R.id.activitytesturlLinearLayout2);
|
||||
mListPopupWindow.setAnchorView(mllAnchorView);
|
||||
mListPopupWindow.setModal(false);
|
||||
// 透明度
|
||||
//listPopupWindow.setBackgroundDrawable(new ColorDrawable(0xEEAAAAAA));
|
||||
mListPopupWindow.setBackgroundDrawable(mContext.getDrawable(R.drawable.bg_shadow));
|
||||
|
||||
mListPopupWindow.setOnItemClickListener(new AdapterView.OnItemClickListener() {//设置项点击监听
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
|
||||
mURLAddressEditText.setText(list[i]);//展示选择的内容
|
||||
postposesURL(list[i]);
|
||||
updateFavoriteButtonStatus();
|
||||
//setWebViewURL(list[i]);
|
||||
mListPopupWindow.dismiss();//如果已经选择了,隐藏起来
|
||||
mnRightImageViewTouchCount = 0;
|
||||
}
|
||||
});
|
||||
mListPopupWindow.setOnDismissListener(new PopupWindow.OnDismissListener(){
|
||||
@Override
|
||||
public void onDismiss() {
|
||||
mIsListPopupWindow_OnShow = false;
|
||||
}
|
||||
});
|
||||
mIsListPopupWindow_OnShow = true;
|
||||
mListPopupWindow.show();//下拉列表展示出来
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
41
webpagesources/src/main/res/drawable/bg_shadow.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<!-- 阴影部分 -->
|
||||
<!-- 个人觉得更形象的表达:top代表下边的阴影高度,left代表右边的阴影宽度。其实也就是相对应的offset,solid中的颜色是阴影的颜色,也可以设置角度等等 -->
|
||||
<item
|
||||
android:left="2dp"
|
||||
android:top="2dp"
|
||||
android:right="2dp"
|
||||
android:bottom="2dp">
|
||||
<shape android:shape="rectangle" >
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:endColor="#0F000000"
|
||||
android:startColor="#0F000000" />
|
||||
<corners
|
||||
android:bottomLeftRadius="6dip"
|
||||
android:bottomRightRadius="6dip"
|
||||
android:topLeftRadius="6dip"
|
||||
android:topRightRadius="6dip" />
|
||||
</shape>
|
||||
</item>
|
||||
<!-- 背景部分 -->
|
||||
<!-- 形象的表达:bottom代表背景部分在上边缘超出阴影的高度,right代表背景部分在左边超出阴影的宽度(相对应的offset) -->
|
||||
<item
|
||||
android:left="3dp"
|
||||
android:top="3dp"
|
||||
android:right="3dp"
|
||||
android:bottom="5dp">
|
||||
<shape android:shape="rectangle" >
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:endColor="@color/colorAccent"
|
||||
android:startColor="@color/colorAccent" />
|
||||
<corners
|
||||
android:bottomLeftRadius="6dip"
|
||||
android:bottomRightRadius="6dip"
|
||||
android:topLeftRadius="6dip"
|
||||
android:topRightRadius="6dip" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
11
webpagesources/src/main/res/drawable/ic_code_block_html.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorAppTextColor"
|
||||
android:pathData="M5.59,3.41L7,4.82L3.82,8L7,11.18L5.59,12.6L1,8L5.59,3.41M11.41,3.41L16,8L11.41,12.6L10,11.18L13.18,8L10,4.82L11.41,3.41M22,6V18C22,19.11 21.11,20 20,20H4C2.9,20 2,19.11 2,18V14H4V18H20V6H17.03V4H20C21.11,4 22,4.89 22,6Z"/>
|
||||
|
||||
</vector>
|
||||
11
webpagesources/src/main/res/drawable/ic_favorite.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="@color/colorAppTextColor"
|
||||
android:pathData="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z"/>
|
||||
|
||||
</vector>
|
||||
11
webpagesources/src/main/res/drawable/ic_favorite_outline.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="@color/colorAppTextColor"
|
||||
android:pathData="M12,15.39L8.24,17.66L9.23,13.38L5.91,10.5L10.29,10.13L12,6.09L13.71,10.13L18.09,10.5L14.77,13.38L15.76,17.66M22,9.24L14.81,8.63L12,2L9.19,8.63L2,9.24L7.45,13.97L5.82,21L12,17.27L18.18,21L16.54,13.97L22,9.24Z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorAppTextColor"
|
||||
android:pathData="M10,20H6V4H13V9H18V12.1L20,10.1V8L14,2H6C4.9,2 4,2.9 4,4V20C4,21.1 4.9,22 6,22H10V20M20.2,13C20.3,13 20.5,13.1 20.6,13.2L21.9,14.5C22.1,14.7 22.1,15.1 21.9,15.3L20.9,16.3L18.8,14.2L19.8,13.2C19.9,13.1 20,13 20.2,13M20.2,16.9L14.1,23H12V20.9L18.1,14.8L20.2,16.9Z"/>
|
||||
|
||||
</vector>
|
||||
11
webpagesources/src/main/res/drawable/ic_form_dropdown.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="@color/colorAppTextColor"
|
||||
android:pathData="M17,5H20L18.5,7L17,5M3,2H21C22.11,2 23,2.9 23,4V8C23,9.11 22.11,10 21,10H16V20C16,21.11 15.11,22 14,22H3C1.9,22 1,21.11 1,20V4C1,2.9 1.9,2 3,2M3,4V8H14V4H3M21,8V4H16V8H21M3,20H14V10H3V20M5,12H12V14H5V12M5,16H12V18H5V16Z"/>
|
||||
|
||||
</vector>
|
||||
11
webpagesources/src/main/res/drawable/ic_launcher.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_foreground"/>
|
||||
</layer-list>
|
||||
170
webpagesources/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="@color/colorPrimary"
|
||||
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>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M16.61,15.15C16.15,15.15 15.77,14.78 15.77,14.32S16.15,13.5 16.61,13.5H16.61C17.07,13.5 17.45,13.86 17.45,14.32C17.45,14.78 17.07,15.15 16.61,15.15M7.41,15.15C6.95,15.15 6.57,14.78 6.57,14.32C6.57,13.86 6.95,13.5 7.41,13.5H7.41C7.87,13.5 8.24,13.86 8.24,14.32C8.24,14.78 7.87,15.15 7.41,15.15M16.91,10.14L18.58,7.26C18.67,7.09 18.61,6.88 18.45,6.79C18.28,6.69 18.07,6.75 18,6.92L16.29,9.83C14.95,9.22 13.5,8.9 12,8.91C10.47,8.91 9,9.24 7.73,9.82L6.04,6.91C5.95,6.74 5.74,6.68 5.57,6.78C5.4,6.87 5.35,7.08 5.44,7.25L7.1,10.13C4.25,11.69 2.29,14.58 2,18H22C21.72,14.59 19.77,11.7 16.91,10.14H16.91Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M16.36,14C16.44,13.34 16.5,12.68 16.5,12C16.5,11.32 16.44,10.66 16.36,10H19.74C19.9,10.64 20,11.31 20,12C20,12.69 19.9,13.36 19.74,14M14.59,19.56C15.19,18.45 15.65,17.25 15.97,16H18.92C17.96,17.65 16.43,18.93 14.59,19.56M14.34,14H9.66C9.56,13.34 9.5,12.68 9.5,12C9.5,11.32 9.56,10.65 9.66,10H14.34C14.43,10.65 14.5,11.32 14.5,12C14.5,12.68 14.43,13.34 14.34,14M12,19.96C11.17,18.76 10.5,17.43 10.09,16H13.91C13.5,17.43 12.83,18.76 12,19.96M8,8H5.08C6.03,6.34 7.57,5.06 9.4,4.44C8.8,5.55 8.35,6.75 8,8M5.08,16H8C8.35,17.25 8.8,18.45 9.4,19.56C7.57,18.93 6.03,17.65 5.08,16M4.26,14C4.1,13.36 4,12.69 4,12C4,11.31 4.1,10.64 4.26,10H7.64C7.56,10.66 7.5,11.32 7.5,12C7.5,12.68 7.56,13.34 7.64,14M12,4.03C12.83,5.23 13.5,6.57 13.91,8H10.09C10.5,6.57 11.17,5.23 12,4.03M18.92,8H15.97C15.65,6.75 15.19,5.55 14.59,4.44C16.43,5.07 17.96,6.34 18.92,8M12,2C6.47,2 2,6.5 2,12A10,10 0,0 0,12 22A10,10 0,0 0,22 12A10,10 0,0 0,12 2Z"/>
|
||||
</vector>
|
||||
11
webpagesources/src/main/res/drawable/ic_web.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorAppTextColor"
|
||||
android:pathData="M16.36,14C16.44,13.34 16.5,12.68 16.5,12C16.5,11.32 16.44,10.66 16.36,10H19.74C19.9,10.64 20,11.31 20,12C20,12.69 19.9,13.36 19.74,14M14.59,19.56C15.19,18.45 15.65,17.25 15.97,16H18.92C17.96,17.65 16.43,18.93 14.59,19.56M14.34,14H9.66C9.56,13.34 9.5,12.68 9.5,12C9.5,11.32 9.56,10.65 9.66,10H14.34C14.43,10.65 14.5,11.32 14.5,12C14.5,12.68 14.43,13.34 14.34,14M12,19.96C11.17,18.76 10.5,17.43 10.09,16H13.91C13.5,17.43 12.83,18.76 12,19.96M8,8H5.08C6.03,6.34 7.57,5.06 9.4,4.44C8.8,5.55 8.35,6.75 8,8M5.08,16H8C8.35,17.25 8.8,18.45 9.4,19.56C7.57,18.93 6.03,17.65 5.08,16M4.26,14C4.1,13.36 4,12.69 4,12C4,11.31 4.1,10.64 4.26,10H7.64C7.56,10.66 7.5,11.32 7.5,12C7.5,12.68 7.56,13.34 7.64,14M12,4.03C12.83,5.23 13.5,6.57 13.91,8H10.09C10.5,6.57 11.17,5.23 12,4.03M18.92,8H15.97C15.65,6.75 15.19,5.55 14.59,4.44C16.43,5.07 17.96,6.34 18.92,8M12,2C6.47,2 2,6.5 2,12A10,10 0,0 0,12 22A10,10 0,0 0,22 12A10,10 0,0 0,12 2Z"/>
|
||||
|
||||
</vector>
|
||||